More Cleanup

This commit is contained in:
Qstick 2020-10-19 01:07:21 -04:00
parent d4e12aa276
commit ad04d0d261
550 changed files with 326 additions and 31626 deletions

View file

@ -1,305 +0,0 @@
# -*- coding: utf-8; mode: python -*-
##
## Format
##
## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
##
## Description
##
## ACTION is one of 'chg', 'fix', 'new'
##
## Is WHAT the change is about.
##
## 'chg' is for refactor, small improvement, cosmetic changes...
## 'fix' is for bug fixes
## 'new' is for new features, big improvement
##
## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
##
## Is WHO is concerned by the change.
##
## 'dev' is for developpers (API changes, refactors...)
## 'usr' is for final users (UI changes)
## 'pkg' is for packagers (packaging changes)
## 'test' is for testers (test only related changes)
## 'doc' is for doc guys (doc only changes)
##
## COMMIT_MSG is ... well ... the commit message itself.
##
## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
##
## They are preceded with a '!' or a '@' (prefer the former, as the
## latter is wrongly interpreted in github.) Commonly used tags are:
##
## 'refactor' is obviously for refactoring code only
## 'minor' is for a very meaningless change (a typo, adding a comment)
## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
## 'wip' is for partial functionality but complete subfunctionality.
##
## Example:
##
## new: usr: support of bazaar implemented
## chg: re-indentend some lines !cosmetic
## new: dev: updated code to be compatible with last version of killer lib.
## fix: pkg: updated year of licence coverage.
## new: test: added a bunch of test around user usability of feature X.
## fix: typo in spelling my name in comment. !minor
##
## Please note that multi-line commit message are supported, and only the
## first line will be considered as the "summary" of the commit message. So
## tags, and other rules only applies to the summary. The body of the commit
## message will be displayed in the changelog without reformatting.
##
## ``ignore_regexps`` is a line of regexps
##
## Any commit having its full commit message matching any regexp listed here
## will be ignored and won't be reported in the changelog.
##
ignore_regexps = [
r'@minor', r'!minor',
r'@cosmetic', r'!cosmetic',
r'@refactor', r'!refactor',
r'@wip', r'!wip',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
r'^$', ## ignore commits with empty messages
]
## ``section_regexps`` is a list of 2-tuples associating a string label and a
## list of regexp
##
## Commit messages will be classified in sections thanks to this. Section
## titles are the label, and a commit is classified under this section if any
## of the regexps associated is matching.
##
## Please note that ``section_regexps`` will only classify commits and won't
## make any changes to the contents. So you'll probably want to go check
## ``subject_process`` (or ``body_process``) to do some changes to the subject,
## whenever you are tweaking this variable.
##
section_regexps = [
('**New features**', [
r'^[aA]dded?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?(.*)$',
r'^[uU]pdated?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[cC]hanged?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[nN]ew?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
]),
('**Fixes**', [
r'^(?![mM]erge\s*)'
]
),
]
## ``body_process`` is a callable
##
## This callable will be given the original body and result will
## be used in the changelog.
##
## Available constructs are:
##
## - any python callable that take one txt argument and return txt argument.
##
## - ReSub(pattern, replacement): will apply regexp substitution.
##
## - Indent(chars=" "): will indent the text with the prefix
## Please remember that template engines gets also to modify the text and
## will usually indent themselves the text if needed.
##
## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
##
## - noop: do nothing
##
## - ucfirst: ensure the first letter is uppercase.
## (usually used in the ``subject_process`` pipeline)
##
## - final_dot: ensure text finishes with a dot
## (usually used in the ``subject_process`` pipeline)
##
## - strip: remove any spaces before or after the content of the string
##
## - SetIfEmpty(msg="No commit message."): will set the text to
## whatever given ``msg`` if the current text is empty.
##
## Additionally, you can `pipe` the provided filters, for instance:
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
#body_process = noop
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
## ``subject_process`` is a callable
##
## This callable will be given the original subject and result will
## be used in the changelog.
## subject_process = (strip |
## ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'![New](https://img.shields.io/badge/-- -New-brightgreen.svg?style=flat-square) \4') |
## ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'![Changed](https://img.shields.io/badge/-- -Changed-orange.svg?style=flat-square) \4') |
## ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'![Fixed](https://img.shields.io/badge/-- -Fixed-red.svg?style=flat-square) \4') |
## ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'![Updated](https://img.shields.io/badge/-- -Updated-blue.svg?style=flat-square) \4') |
## ReSub(r'#(\d{3,4})', r'[#\1](https://github.com/Prowlarr/Prowlarr/issues/\1)') |
## SetIfEmpty("No commit message.") | ucfirst | final_dot)
## Available constructs are those listed in ``body_process`` doc.
subject_process = (strip |
ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'![New](https://img.shields.io/badge/--%20-New-brightgreen.svg?style=flat-square) \4') |
ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'![Changed](https://img.shields.io/badge/--%20-Changed-orange.svg?style=flat-square) \4') |
ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'![Fixed](https://img.shields.io/badge/--%20-Fixed-red.svg?style=flat-square) \4') |
ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'![Updated](https://img.shields.io/badge/--%20-Updated-blue.svg?style=flat-square) \4') |
ReSub(r'#(\d{3,4})', r'[#\1](https://github.com/Prowlarr/Prowlarr/issues/\1)') |
SetIfEmpty("No commit message.") | ucfirst | final_dot)
## ``tag_filter_regexp`` is a regexp
##
## Tags that will be used for the changelog must match this regexp.
##
tag_filter_regexp = r'^v[0]+\.[2-9]+\.[0-9]+\.[0-9]{3,4}$'
## ``unreleased_version_label`` is a string or a callable that outputs a string
##
## This label will be used as the changelog Title of the last set of changes
## between last valid tag and HEAD if any.
unreleased_version_label = "(unreleased)"
## ``output_engine`` is a callable
##
## This will change the output format of the generated changelog file
##
## Available choices are:
##
## - rest_py
##
## Legacy pure python engine, outputs ReSTructured text.
## This is the default.
##
## - mustache(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mustache/*.tpl``.
## Requires python package ``pystache``.
## Examples:
## - mustache("markdown")
## - mustache("restructuredtext")
##
## - makotemplate(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mako/*.tpl``.
## Requires python package ``mako``.
## Examples:
## - makotemplate("restructuredtext")
##
#output_engine = rest_py
#output_engine = mustache("restructuredtext")
output_engine = mustache("changelog.tpl")
#output_engine = makotemplate("restructuredtext")
## ``include_merge`` is a boolean
##
## This option tells git-log whether to include merge commits in the log.
## The default is to include them.
include_merge = False
## ``log_encoding`` is a string identifier
##
## This option tells gitchangelog what encoding is outputed by ``git log``.
## The default is to be clever about it: it checks ``git config`` for
## ``i18n.logOutputEncoding``, and if not found will default to git's own
## default: ``utf-8``.
#log_encoding = 'utf-8'
## ``publish`` is a callable
##
## Sets what ``gitchangelog`` should do with the output generated by
## the output engine. ``publish`` is a callable taking one argument
## that is an interator on lines from the output engine.
##
## Some helper callable are provided:
##
## Available choices are:
##
## - stdout
##
## Outputs directly to standard output
## (This is the default)
##
## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start())
##
## Creates a callable that will parse given file for the given
## regex pattern and will insert the output in the file.
## ``idx`` is a callable that receive the matching object and
## must return a integer index point where to insert the
## the output in the file. Default is to return the position of
## the start of the matched string.
##
## - FileRegexSubst(file, pattern, replace, flags)
##
## Apply a replace inplace in the given file. Your regex pattern must
## take care of everything and might be more complex. Check the README
## for a complete copy-pastable example.
##
# publish = FileInsertIntoFirstRegexMatch(
# "CHANGELOG.rst",
# r'/(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/',
# idx=lambda m: m.start(1)
# )
#publish = stdout
def write_to_file(content):
with open("CHANGELOG.md", "w+") as f:
for chunk in content:
f.write(chunk)
publish = write_to_file
## ``revs`` is a list of callable or a list of string
##
## callable will be called to resolve as strings and allow dynamical
## computation of these. The result will be used as revisions for
## gitchangelog (as if directly stated on the command line). This allows
## to filter exaclty which commits will be read by gitchangelog.
##
## To get a full documentation on the format of these strings, please
## refer to the ``git rev-list`` arguments. There are many examples.
##
## Using callables is especially useful, for instance, if you
## are using gitchangelog to generate incrementally your changelog.
##
## Some helpers are provided, you can use them::
##
## - FileFirstRegexMatch(file, pattern): will return a callable that will
## return the first string match for the given pattern in the given file.
## If you use named sub-patterns in your regex pattern, it'll output only
## the string matching the regex pattern named "rev".
##
## - Caret(rev): will return the rev prefixed by a "^", which is a
## way to remove the given revision and all its ancestor.
##
## Please note that if you provide a rev-list on the command line, it'll
## replace this value (which will then be ignored).
##
## If empty, then ``gitchangelog`` will act as it had to generate a full
## changelog.
##
## The default is to use all commits to make the changelog.
#revs = ["^1.0.3", ]
#revs = [
# Caret(
# FileFirstRegexMatch(
# "CHANGELOG.rst",
# r"(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")),
# "HEAD"
#]
revs = ["v0.2.0.134..."]

View file

@ -1,311 +0,0 @@
# -*- coding: utf-8; mode: python -*-
##
## Format
##
## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
##
## Description
##
## ACTION is one of 'chg', 'fix', 'new'
##
## Is WHAT the change is about.
##
## 'chg' is for refactor, small improvement, cosmetic changes...
## 'fix' is for bug fixes
## 'new' is for new features, big improvement
##
## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
##
## Is WHO is concerned by the change.
##
## 'dev' is for developpers (API changes, refactors...)
## 'usr' is for final users (UI changes)
## 'pkg' is for packagers (packaging changes)
## 'test' is for testers (test only related changes)
## 'doc' is for doc guys (doc only changes)
##
## COMMIT_MSG is ... well ... the commit message itself.
##
## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
##
## They are preceded with a '!' or a '@' (prefer the former, as the
## latter is wrongly interpreted in github.) Commonly used tags are:
##
## 'refactor' is obviously for refactoring code only
## 'minor' is for a very meaningless change (a typo, adding a comment)
## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
## 'wip' is for partial functionality but complete subfunctionality.
##
## Example:
##
## new: usr: support of bazaar implemented
## chg: re-indentend some lines !cosmetic
## new: dev: updated code to be compatible with last version of killer lib.
## fix: pkg: updated year of licence coverage.
## new: test: added a bunch of test around user usability of feature X.
## fix: typo in spelling my name in comment. !minor
##
## Please note that multi-line commit message are supported, and only the
## first line will be considered as the "summary" of the commit message. So
## tags, and other rules only applies to the summary. The body of the commit
## message will be displayed in the changelog without reformatting.
##
## ``ignore_regexps`` is a line of regexps
##
## Any commit having its full commit message matching any regexp listed here
## will be ignored and won't be reported in the changelog.
##
ignore_regexps = [
r'@minor', r'!minor',
r'@cosmetic', r'!cosmetic',
r'@refactor', r'!refactor',
r'@wip', r'!wip',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
r'^$', ## ignore commits with empty messages
]
## ``section_regexps`` is a list of 2-tuples associating a string label and a
## list of regexp
##
## Commit messages will be classified in sections thanks to this. Section
## titles are the label, and a commit is classified under this section if any
## of the regexps associated is matching.
##
## Please note that ``section_regexps`` will only classify commits and won't
## make any changes to the contents. So you'll probably want to go check
## ``subject_process`` (or ``body_process``) to do some changes to the subject,
## whenever you are tweaking this variable.
##
section_regexps = [
('**New features:**', [
r'^[aA]dded?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[uU]pdated?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[cC]hanged?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
r'^[nN]ew?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
]),
('**Fixes:**', [
r'^(?![mM]erge\s*)'
]
),
]
## ``body_process`` is a callable
##
## This callable will be given the original body and result will
## be used in the changelog.
##
## Available constructs are:
##
## - any python callable that take one txt argument and return txt argument.
##
## - ReSub(pattern, replacement): will apply regexp substitution.
##
## - Indent(chars=" "): will indent the text with the prefix
## Please remember that template engines gets also to modify the text and
## will usually indent themselves the text if needed.
##
## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
##
## - noop: do nothing
##
## - ucfirst: ensure the first letter is uppercase.
## (usually used in the ``subject_process`` pipeline)
##
## - final_dot: ensure text finishes with a dot
## (usually used in the ``subject_process`` pipeline)
##
## - strip: remove any spaces before or after the content of the string
##
## - SetIfEmpty(msg="No commit message."): will set the text to
## whatever given ``msg`` if the current text is empty.
##
## Additionally, you can `pipe` the provided filters, for instance:
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
#body_process = noop
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
## ``subject_process`` is a callable
##
## This callable will be given the original subject and result will
## be used in the changelog.
## subject_process = (strip |
## ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'![New](https://img.shields.io/badge/-- -New-brightgreen.svg?style=flat-square) \4') |
## ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'![Changed](https://img.shields.io/badge/-- -Changed-orange.svg?style=flat-square) \4') |
## ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'![Fixed](https://img.shields.io/badge/-- -Fixed-red.svg?style=flat-square) \4') |
## ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'![Updated](https://img.shields.io/badge/-- -Updated-blue.svg?style=flat-square) \4') |
## ReSub(r'#(\d{3,4})', r'[#\1](https://github.com/Prowlarr/Prowlarr/issues/\1)') |
## SetIfEmpty("No commit message.") | ucfirst | final_dot)
## Available constructs are those listed in ``body_process`` doc.
subject_process = (strip |
ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'^([cC]hang(ed?)?)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'^([fF]ix(ed?)?)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'^([uU]pdat(ed?)?)(\s?:?\s)(.*)$', r'\4') |
ReSub(r'#(\d{3,4})', r'Issue #\1') |
SetIfEmpty("No commit message.") | ucfirst | final_dot)
## ``tag_filter_regexp`` is a regexp
##
## Tags that will be used for the changelog must match this regexp.
##
tag_filter_regexp = r'^v[0]+\.[2-9]+\.[0-9]+\.[0-9]+$'
## ``unreleased_version_label`` is a string or a callable that outputs a string
##
## This label will be used as the changelog Title of the last set of changes
## between last valid tag and HEAD if any.
unreleased_version_label = "(unreleased)"
## ``output_engine`` is a callable
##
## This will change the output format of the generated changelog file
##
## Available choices are:
##
## - rest_py
##
## Legacy pure python engine, outputs ReSTructured text.
## This is the default.
##
## - mustache(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mustache/*.tpl``.
## Requires python package ``pystache``.
## Examples:
## - mustache("markdown")
## - mustache("restructuredtext")
##
## - makotemplate(<template_name>)
##
## Template name could be any of the available templates in
## ``templates/mako/*.tpl``.
## Requires python package ``mako``.
## Examples:
## - makotemplate("restructuredtext")
##
#output_engine = rest_py
#output_engine = mustache("restructuredtext")
output_engine = mustache("changelog_release.tpl")
#output_engine = makotemplate("restructuredtext")
## ``include_merge`` is a boolean
##
## This option tells git-log whether to include merge commits in the log.
## The default is to include them.
include_merge = False
## ``log_encoding`` is a string identifier
##
## This option tells gitchangelog what encoding is outputed by ``git log``.
## The default is to be clever about it: it checks ``git config`` for
## ``i18n.logOutputEncoding``, and if not found will default to git's own
## default: ``utf-8``.
#log_encoding = 'utf-8'
## ``publish`` is a callable
##
## Sets what ``gitchangelog`` should do with the output generated by
## the output engine. ``publish`` is a callable taking one argument
## that is an interator on lines from the output engine.
##
## Some helper callable are provided:
##
## Available choices are:
##
## - stdout
##
## Outputs directly to standard output
## (This is the default)
##
## - FileInsertAtFirstRegexMatch(file, pattern, idx=lamda m: m.start())
##
## Creates a callable that will parse given file for the given
## regex pattern and will insert the output in the file.
## ``idx`` is a callable that receive the matching object and
## must return a integer index point where to insert the
## the output in the file. Default is to return the position of
## the start of the matched string.
##
## - FileRegexSubst(file, pattern, replace, flags)
##
## Apply a replace inplace in the given file. Your regex pattern must
## take care of everything and might be more complex. Check the README
## for a complete copy-pastable example.
##
# publish = FileInsertIntoFirstRegexMatch(
# "CHANGELOG.rst",
# r'/(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n/',
# idx=lambda m: m.start(1)
# )
publish = stdout
#def write_to_file(content):
# with open("CHANGELOG.md", "w+") as f:
# for chunk in content:
# f.write(chunk)
#publish = write_to_file
## ``revs`` is a list of callable or a list of string
##
## callable will be called to resolve as strings and allow dynamical
## computation of these. The result will be used as revisions for
## gitchangelog (as if directly stated on the command line). This allows
## to filter exaclty which commits will be read by gitchangelog.
##
## To get a full documentation on the format of these strings, please
## refer to the ``git rev-list`` arguments. There are many examples.
##
## Using callables is especially useful, for instance, if you
## are using gitchangelog to generate incrementally your changelog.
##
## Some helpers are provided, you can use them::
##
## - FileFirstRegexMatch(file, pattern): will return a callable that will
## return the first string match for the given pattern in the given file.
## If you use named sub-patterns in your regex pattern, it'll output only
## the string matching the regex pattern named "rev".
##
## - Caret(rev): will return the rev prefixed by a "^", which is a
## way to remove the given revision and all its ancestor.
##
## Please note that if you provide a rev-list on the command line, it'll
## replace this value (which will then be ignored).
##
## If empty, then ``gitchangelog`` will act as it had to generate a full
## changelog.
##
## The default is to use all commits to make the changelog.
#revs = ["^1.0.3", ]
#revs = [
# Caret(
# FileFirstRegexMatch(
# "CHANGELOG.rst",
# r"(?P<rev>[0-9]+\.[0-9]+(\.[0-9]+)?)\s+\([0-9]+-[0-9]{2}-[0-9]{2}\)\n--+\n")),
# "HEAD"
#]
# Gets the latest annoted tag and uses that as a base for new changes.
import subprocess
proc = subprocess.Popen(["git", "describe", "--abbrev=0", "--tags"], stdout=subprocess.PIPE)
out = str(proc.communicate()[0].strip(), "utf-8")
revs = [out+"..."]

6
.gitignore vendored
View file

@ -139,9 +139,9 @@ artifacts/
**/Properties/launchSettings.json **/Properties/launchSettings.json
# Packages # Packages
Radarr_*/ Prowlarr_*/
Radarr_*.zip Prowlarr_*.zip
Radarr_*.gz Prowlarr_*.gz
gecko.zip gecko.zip
geckodriver.exe geckodriver.exe

View file

@ -1,3 +1,3 @@
# Prowlarr # Prowlarr
Prowlarr is a indexer manager/proxy built on the popular arr .net base stack to integrate with your various PVR apps. Prowlarr supports both Torrent and Usenet indexers and integrates seamlessly with Sonarr, Radarr, Lidarr, Readarr offering complete management of your indexers with no per app Indexer setup required (we do it all). Prowlarr is a indexer manager/proxy built on the popular arr .net base stack to integrate with your various PVR apps. Prowlarr supports both Torrent and Usenet indexers and integrates seamlessly with Sonarr, Prowlarr, Lidarr, Readarr offering complete management of your indexers with no per app Indexer setup required (we do it all).

View file

@ -7,13 +7,13 @@ variables:
outputFolder: './_output' outputFolder: './_output'
artifactsFolder: './_artifacts' artifactsFolder: './_artifacts'
testsFolder: './_tests' testsFolder: './_tests'
majorVersion: '3.0.0' majorVersion: '0.1.0'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 1)]
radarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '3.1.401' dotnetVersion: '3.1.403'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger: trigger:
@ -35,7 +35,7 @@ stages:
vmImage: 'ubuntu-18.04' vmImage: 'ubuntu-18.04'
steps: steps:
# Set the build name properly. The 'name' property won't recursively expand so hack here: # Set the build name properly. The 'name' property won't recursively expand so hack here:
- bash: echo "##vso[build.updatebuildnumber]$RADARRVERSION" - bash: echo "##vso[build.updatebuildnumber]$PROWLARRVERSION"
displayName: Set Build Name displayName: Set Build Name
- bash: | - bash: |
if [[ $BUILD_REASON == "PullRequest" ]]; then if [[ $BUILD_REASON == "PullRequest" ]]; then
@ -297,7 +297,7 @@ stages:
- bash: | - bash: |
echo "Uploading source maps to sentry" echo "Uploading source maps to sentry"
curl -sL https://sentry.io/get-cli/ | bash curl -sL https://sentry.io/get-cli/ | bash
RELEASENAME="${RADARRVERSION}-${BUILD_SOURCEBRANCHNAME}" RELEASENAME="${PROWLARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
sentry-cli releases new --finalize -p prowlarr -p prowlarr-ui -p prowlarr-update "${RELEASENAME}" sentry-cli releases new --finalize -p prowlarr -p prowlarr-ui -p prowlarr-update "${RELEASENAME}"
sentry-cli releases -p prowlarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite sentry-cli releases -p prowlarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
sentry-cli releases set-commits --auto "${RELEASENAME}" sentry-cli releases set-commits --auto "${RELEASENAME}"
@ -376,13 +376,6 @@ stages:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)Tests' artifactName: '$(testName)Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- bash: |
wget https://mediaarea.net/repo/deb/repo-mediaarea_1.0-11_all.deb
sudo dpkg -i repo-mediaarea_1.0-11_all.deb
sudo apt-get update
sudo apt-get install -y --allow-unauthenticated libmediainfo-dev libmediainfo0v5 mediainfo
displayName: Install mediainfo
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
@ -764,27 +757,6 @@ stages:
FORCE_COLOR: 0 FORCE_COLOR: 0
YARN_CACHE_FOLDER: $(yarnCacheFolder) YARN_CACHE_FOLDER: $(yarnCacheFolder)
- job: Analyze_Frontend
displayName: Frontend
condition: eq(variables['System.PullRequest.IsFork'], 'False')
pool:
vmImage: windows-2019
steps:
- checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@1
env:
SONAR_SCANNER_OPTS: ''
inputs:
SonarCloud: 'SonarCloud'
organization: 'prowlarr'
scannerMode: 'CLI'
configMode: 'manual'
cliProjectKey: 'Radarr_Radarr.UI'
cliProjectName: 'RadarrUI'
cliProjectVersion: '$(radarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@1
- job: Analyze_Backend - job: Analyze_Backend
displayName: Backend displayName: Backend
dependsOn: Prepare dependsOn: Prepare
@ -805,27 +777,10 @@ stages:
submodules: true submodules: true
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
- task: SonarCloudPrepare@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
inputs:
SonarCloud: 'SonarCloud'
organization: 'prowlarr'
scannerMode: 'MSBuild'
projectKey: 'Radarr_Radarr'
projectName: 'Prowlarr'
projectVersion: '$(radarrVersion)'
extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Prowlarr.Api.V1/**/*,**/NzbDrone.Api/**/*,**/MonoTorrent/**/*,**/Marr.Data/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: | - bash: |
./build.sh --backend -f netcoreapp3.1 -r win-x64 ./build.sh --backend -f netcoreapp3.1 -r win-x64
TEST_DIR=_tests/netcoreapp3.1/win-x64/publish/ ./test.sh Windows Unit Coverage TEST_DIR=_tests/netcoreapp3.1/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@4 - task: reportgenerator@4
displayName: Generate Coverage Report displayName: Generate Coverage Report
inputs: inputs:

View file

@ -17,11 +17,11 @@ ProgressEnd()
UpdateVersionNumber() UpdateVersionNumber()
{ {
if [ "$RADARRVERSION" != "" ]; then if [ "$PROWLARRVERSION" != "" ]; then
echo "Updating Version Info" echo "Updating Version Info"
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$RADARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$PROWLARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props
sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$RADARRVERSION<\/string>/g" macOS/Prowlarr.app/Contents/Info.plist sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$PROWLARRVERSION<\/string>/g" macOS/Prowlarr.app/Contents/Info.plist
fi fi
} }

View file

@ -1,235 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
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 BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// 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);
});
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isRemoving,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blacklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadBlacklist')}
</div>
}
{
isPopulated && !error && !items.length &&
<div>
{translate('NoHistory')}
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<BlacklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blacklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
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,
isRemoving: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};
export default Blacklist;

View file

@ -1,160 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blacklist from './Blacklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, isClearingBlacklistExecuting) => {
return {
isClearingBlacklistExecuting,
...blacklist
};
}
);
}
const mapDispatchToProps = {
...blacklistActions,
executeCommand
};
class BlacklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlacklist,
gotoBlacklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlacklist();
} else {
gotoBlacklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlacklist();
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 });
}
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
}
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 });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
//
// Render
render() {
return (
<Blacklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
{...this.props}
/>
);
}
}
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
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,
removeBlacklistItems: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
);

View file

@ -1,90 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate';
class BlacklistDetailsModal extends Component {
//
// Render
render() {
const {
isOpen,
sourceTitle,
protocol,
indexer,
message,
onModalClose
} = this.props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Details
</ModalHeader>
<ModalBody>
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Protocol')}
data={protocol}
/>
{
!!message &&
<DescriptionListItem
title={translate('Indexer')}
data={indexer}
/>
}
{
!!message &&
<DescriptionListItem
title={translate('Message')}
data={message}
/>
}
</DescriptionList>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
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;

View file

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

View file

@ -1,213 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import translate from 'Utilities/String/translate';
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 {
id,
movie,
sourceTitle,
quality,
customFormats,
languages,
date,
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
onRemovePress
} = this.props;
if (!movie) {
return null;
}
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'movies.sortTitle') {
return (
<TableRowCell key={name}>
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/>
</TableRowCell>
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell key={name}>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell
key={name}
className={styles.quality}
>
<MovieQuality
quality={quality}
/>
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCellConnector
key={name}
date={date}
/>
);
}
if (name === 'indexer') {
return (
<TableRowCell
key={name}
className={styles.indexer}
>
{indexer}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
key={name}
className={styles.actions}
>
<IconButton
name={icons.INFO}
onPress={this.onDetailsPress}
/>
<IconButton
title={translate('RemoveFromBlacklist')}
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
/>
</TableRowCell>
);
}
return null;
})
}
<BlacklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
indexer={indexer}
message={message}
onModalClose={this.onDetailsModalClose}
/>
</TableRow>
);
}
}
BlacklistRow.propTypes = {
id: PropTypes.number.isRequired,
movie: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
date: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};
export default BlacklistRow;

View file

@ -1,26 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector';
import BlacklistRow from './BlacklistRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
(movie) => {
return {
movie
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeBlacklistItem({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);

View file

@ -1,13 +0,0 @@
.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;
}

View file

@ -1,20 +0,0 @@
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 (
<Label className={styles[protocol]}>
{protocolName}
</Label>
);
}
ProtocolLabel.propTypes = {
protocol: PropTypes.string.isRequired
};
export default ProtocolLabel;

View file

@ -1,292 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
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 QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
class Queue extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isPendingSelected: false,
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items,
isFetching,
isMoviesFetching
} = this.props;
if (
(!isMoviesFetching && prevProps.isMoviesFetching) ||
(!isFetching && prevProps.isFetching) ||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
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 = (payload) => {
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
isMoviesFetching,
isMoviesPopulated,
moviesError,
columns,
totalRecords,
isGrabbing,
isRemoving,
isRefreshMonitoredDownloadsExecuting,
onRefreshPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isPendingSelected,
items
} = this.state;
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
const hasError = error || moviesError;
const selectedIds = this.getSelectedIds();
const selectedCount = selectedIds.length;
const disableSelectedActions = selectedCount === 0;
return (
<PageContent title={translate('Queue')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={onRefreshPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('GrabSelected')}
iconName={icons.DOWNLOAD}
isDisabled={disableSelectedActions || !isPendingSelected}
isSpinning={isGrabbing}
onPress={this.onGrabSelectedPress}
/>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={disableSelectedActions}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
>
<TableOptionsModalWrapper
columns={columns}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>
<PageToolbarButton
label={translate('Options')}
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
}
{
!isRefreshing && hasError &&
<div>
Failed to load Queue
</div>
}
{
isPopulated && !hasError && !items.length &&
<div>
Queue is empty
</div>
}
{
isAllPopulated && !hasError && !!items.length &&
<div>
<Table
columns={columns}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
{...otherProps}
optionsComponent={QueueOptionsConnector}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<QueueRowConnector
key={item.id}
movieId={item.movieId}
isSelected={selectedState[item.id]}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isRefreshing}
{...otherProps}
/>
</div>
}
</PageContentBody>
<RemoveQueueItemsModal
isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount}
canIgnore={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.movieId);
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Queue.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
isMoviesFetching: PropTypes.bool.isRequired,
isMoviesPopulated: PropTypes.bool.isRequired,
moviesError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired
};
export default Queue;

View file

@ -1,170 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import * as queueActions from 'Store/Actions/queueActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue';
function createMapStateToProps() {
return createSelector(
(state) => state.movies,
(state) => state.queue.options,
(state) => state.queue.paged,
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
return {
isMoviesFetching: movies.isFetching,
isMoviesPopulated: movies.isPopulated,
moviesError: movies.error,
isRefreshMonitoredDownloadsExecuting,
...options,
...queue
};
}
);
}
const mapDispatchToProps = {
...queueActions,
executeCommand
};
class QueueConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchQueue,
gotoQueueFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchQueue();
} else {
gotoQueueFirstPage();
}
}
componentDidUpdate(prevProps) {
if (
this.props.includeUnknownMovieItems !==
prevProps.includeUnknownMovieItems
) {
this.repopulate();
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearQueue();
}
//
// 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.REFRESH_MONITORED_DOWNLOADS
});
}
onGrabSelectedPress = (ids) => {
this.props.grabQueueItems({ ids });
}
onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload);
}
//
// Render
render() {
return (
<Queue
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onRefreshPress={this.onRefreshPress}
onGrabSelectedPress={this.onGrabSelectedPress}
onRemoveSelectedPress={this.onRemoveSelectedPress}
{...this.props}
/>
);
}
}
QueueConnector.propTypes = {
includeUnknownMovieItems: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
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,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
);

View file

@ -1,128 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function QueueDetails(props) {
const {
title,
size,
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage,
progressBar
} = props;
const progress = size ? (100 - sizeleft / size * 100) : 0;
if (status === 'pending') {
return (
<Icon
name={icons.PENDING}
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])}
/>
);
}
if (status === 'completed') {
if (errorMessage) {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.DANGER}
title={translate('ImportFailedInterp', [errorMessage])}
/>
);
}
if (trackedDownloadStatus === 'warning') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={'Downloaded - Unable to Import: check logs for details'}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('WaitingToImport')}`}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={`${translate('Downloaded')} - ${translate('Importing')}`}
/>
);
}
}
if (errorMessage) {
return (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={translate('DownloadFailedInterp', [errorMessage])}
/>
);
}
if (status === 'failed') {
return (
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')}
/>
);
}
if (status === 'warning') {
return (
<Icon
name={icons.DOWNLOADING}
kind={kinds.WARNING}
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')}
/>
);
}
if (progress < 5) {
return (
<Icon
name={icons.DOWNLOADING}
title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}
/>
);
}
return progressBar;
}
QueueDetails.propTypes = {
title: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};
export default QueueDetails;

View file

@ -1,78 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class QueueOptions extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
includeUnknownMovieItems: props.includeUnknownMovieItems
};
}
componentDidUpdate(prevProps) {
const {
includeUnknownMovieItems
} = this.props;
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
this.setState({
includeUnknownMovieItems
});
}
}
//
// Listeners
onOptionChange = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onOptionChange({
[name]: value
});
});
}
//
// Render
render() {
const {
includeUnknownMovieItems
} = this.state;
return (
<Fragment>
<FormGroup>
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownMovieItems"
value={includeUnknownMovieItems}
helpText={translate('IncludeUnknownMovieItemsHelpText')}
onChange={this.onOptionChange}
/>
</FormGroup>
</Fragment>
);
}
}
QueueOptions.propTypes = {
includeUnknownMovieItems: PropTypes.bool.isRequired,
onOptionChange: PropTypes.func.isRequired
};
export default QueueOptions;

View file

@ -1,19 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setQueueOption } from 'Store/Actions/queueActions';
import QueueOptions from './QueueOptions';
function createMapStateToProps() {
return createSelector(
(state) => state.queue.options,
(options) => {
return options;
}
);
}
const mapDispatchToProps = {
onOptionChange: setQueueOption
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);

View file

@ -1,23 +0,0 @@
.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;
}

View file

@ -1,370 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import TimeleftCell from './TimeleftCell';
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,
trackedDownloadState,
statusMessages,
errorMessage,
movie,
quality,
customFormats,
languages,
protocol,
indexer,
outputPath,
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 (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'status') {
return (
<QueueStatusCell
key={name}
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
/>
);
}
if (name === 'movies.sortTitle') {
return (
<TableRowCell key={name}>
{
movie ?
<MovieTitleLink
titleSlug={movie.titleSlug}
title={movie.title}
/> :
title
}
</TableRowCell>
);
}
if (name === 'languages') {
return (
<TableRowCell key={name}>
<MovieLanguage
languages={languages}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
{
quality ?
<MovieQuality
quality={quality}
/> :
null
}
</TableRowCell>
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<MovieFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'protocol') {
return (
<TableRowCell key={name}>
<ProtocolLabel
protocol={protocol}
/>
</TableRowCell>
);
}
if (name === 'indexer') {
return (
<TableRowCell key={name}>
{indexer}
</TableRowCell>
);
}
if (name === 'downloadClient') {
return (
<TableRowCell key={name}>
{downloadClient}
</TableRowCell>
);
}
if (name === 'size') {
return (
<TableRowCell key={name}>
{formatBytes(size)}
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell key={name}>
{title}
</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
{outputPath}
</TableRowCell>
);
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
key={name}
status={status}
estimatedCompletionTime={estimatedCompletionTime}
timeleft={timeleft}
size={size}
sizeleft={sizeleft}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
/>
);
}
if (name === 'progress') {
return (
<TableRowCell
key={name}
className={styles.progress}
>
{
!!progress &&
<ProgressBar
progress={progress}
title={`${progress.toFixed(1)}%`}
/>
}
</TableRowCell>
);
}
if (name === 'actions') {
return (
<TableRowCell
key={name}
className={styles.actions}
>
{
showInteractiveImport &&
<IconButton
name={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
}
{
isPending &&
<SpinnerIconButton
name={icons.DOWNLOAD}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
isSpinning={isGrabbing}
onPress={onGrabPress}
/>
}
<SpinnerIconButton
title={translate('RemoveFromQueue')}
name={icons.REMOVE}
isSpinning={isRemoving}
onPress={this.onRemoveQueueItemPress}
/>
</TableRowCell>
);
}
return null;
})
}
<InteractiveImportModal
isOpen={isInteractiveImportModalOpen}
downloadId={downloadId}
title={title}
onModalClose={this.onInteractiveImportModalClose}
/>
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!movie}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
</TableRow>
);
}
}
QueueRow.propTypes = {
id: PropTypes.number.isRequired,
downloadId: PropTypes.string,
title: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string,
trackedDownloadState: PropTypes.string,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
movie: PropTypes.object,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
outputPath: 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;

View file

@ -1,67 +0,0 @@
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 createMovieSelector from 'Store/Selectors/createMovieSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import QueueRow from './QueueRow';
function createMapStateToProps() {
return createSelector(
createMovieSelector(),
createUISettingsSelector(),
(movie, uiSettings) => {
const result = {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
result.movie = movie;
return result;
}
);
}
const mapDispatchToProps = {
grabQueueItem,
removeQueueItem
};
class QueueRowConnector extends Component {
//
// Listeners
onGrabPress = () => {
this.props.grabQueueItem({ id: this.props.id });
}
onRemoveQueueItemPress = (payload) => {
this.props.removeQueueItem({ id: this.props.id, ...payload });
}
//
// Render
render() {
return (
<QueueRow
{...this.props}
onGrabPress={this.onGrabPress}
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
/>
);
}
}
QueueRowConnector.propTypes = {
id: PropTypes.number.isRequired,
movie: PropTypes.object,
grabQueueItem: PropTypes.func.isRequired,
removeQueueItem: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);

View file

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

View file

@ -1,157 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './QueueStatusCell.css';
function getDetailedPopoverBody(statusMessages) {
return (
<div>
{
statusMessages.map(({ title, messages }) => {
return (
<div key={title}>
{title}
<ul>
{
messages.map((message) => {
return (
<li key={message}>
{message}
</li>
);
})
}
</ul>
</div>
);
})
}
</div>
);
}
function QueueStatusCell(props) {
const {
sourceTitle,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage
} = props;
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let title = translate('Downloading');
if (status === 'paused') {
iconName = icons.PAUSED;
title = translate('Paused');
}
if (status === 'queued') {
iconName = icons.QUEUED;
title = translate('Queued');
}
if (status === 'completed') {
iconName = icons.DOWNLOADED;
title = translate('Downloaded');
if (trackedDownloadState === 'importPending') {
title += ` - ${translate('WaitingToImport')}`;
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ` - ${translate('Importing')}`;
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ` - ${translate('WaitingToProcess')}`;
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'delay') {
iconName = icons.PENDING;
title = translate('Pending');
}
if (status === 'DownloadClientUnavailable') {
iconName = icons.PENDING;
iconKind = kinds.WARNING;
title = `${translate('Pending')} - ${translate('DownloadClientUnavailable')}`;
}
if (status === 'failed') {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
}
if (status === 'warning') {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
title = translate('DownloadWarning', [warningMessage]);
}
if (hasError) {
if (status === 'completed') {
iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER;
title = translate('ImportFailed', [sourceTitle]);
} else {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = translate('DownloadFailed');
}
}
return (
<TableRowCell className={styles.status}>
<Popover
anchor={
<Icon
name={iconName}
kind={iconKind}
/>
}
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={tooltipPositions.RIGHT}
canFlip={false}
/>
</TableRowCell>
);
}
QueueStatusCell.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string
};
QueueStatusCell.defaultProps = {
trackedDownloadStatus: translate('Ok'),
trackedDownloadState: translate('Downloading')
};
export default QueueStatusCell;

View file

@ -1,144 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class RemoveQueueItemModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blacklist: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blacklist: false
});
}
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
}
onModalClose = () => {
this.resetState();
this.props.onModalClose();
}
//
// Render
render() {
const {
isOpen,
sourceTitle,
canIgnore
} = this.props;
const { remove, blacklist } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
Remove - {sourceTitle}
</ModalHeader>
<ModalBody>
<div>
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('BlacklistRelease')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText={translate('BlacklistHelpText')}
onChange={this.onBlacklistChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
Remove
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemModal;

View file

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

View file

@ -1,148 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemsModal.css';
class RemoveQueueItemsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blacklist: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blacklist: false
});
}
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
}
onModalClose = () => {
this.resetState();
this.props.onModalClose();
}
//
// Render
render() {
const {
isOpen,
selectedCount,
canIgnore
} = this.props;
const { remove, blacklist } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
Remove Selected Item{selectedCount > 1 ? 's' : ''}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div>
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText={translate('BlacklistHelpText')}
onChange={this.onBlacklistChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
Remove
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemsModal;

View file

@ -1,76 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
import { fetchQueueStatus } from 'Store/Actions/queueActions';
function createMapStateToProps() {
return createSelector(
(state) => state.app,
(state) => state.queue.status,
(state) => state.queue.options.includeUnknownMovieItems,
(app, status, includeUnknownMovieItems) => {
const {
errors,
warnings,
unknownErrors,
unknownWarnings,
count,
totalCount
} = status.item;
return {
isConnected: app.isConnected,
isReconnecting: app.isReconnecting,
isPopulated: status.isPopulated,
...status.item,
count: includeUnknownMovieItems ? totalCount : count,
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
};
}
);
}
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 (
<PageSidebarStatus
{...this.props}
/>
);
}
}
QueueStatusConnector.propTypes = {
isConnected: PropTypes.bool.isRequired,
isReconnecting: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
fetchQueueStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);

View file

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

View file

@ -1,83 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
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 translate from 'Utilities/String/translate';
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 (
<TableRowCell
className={styles.timeleft}
title={translate('DelayingDownloadUntilInterp', [date, time])}
>
-
</TableRowCell>
);
}
if (status === 'downloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('RetryingDownloadInterp', [date, time])}
>
-
</TableRowCell>
);
}
if (!timeleft || status === 'completed' || status === 'failed') {
return (
<TableRowCell className={styles.timeleft}>
-
</TableRowCell>
);
}
const totalSize = formatBytes(size);
const remainingSize = formatBytes(sizeleft);
return (
<TableRowCell
className={styles.timeleft}
title={`${remainingSize} / ${totalSize}`}
>
{formatTimeSpan(timeleft)}
</TableRowCell>
);
}
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;

View file

@ -1,60 +0,0 @@
.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: input 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;
font-weight: 300;
font-size: $largeFontSize;
}
.helpText {
margin-bottom: 10px;
font-size: 24px;
}
.noMoviesText {
margin-top: 80px;
margin-bottom: 20px;
}
.noResults {
margin-bottom: 10px;
font-weight: 300;
font-size: 30px;
}
.searchResults {
margin-top: 30px;
}

View file

@ -1,219 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import { icons, kinds } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import AddNewMovieSearchResultConnector from './AddNewMovieSearchResultConnector';
import styles from './AddNewMovie.css';
class AddNewMovie 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.onMovieLookupChange(term);
}
}
componentDidUpdate(prevProps) {
const {
term,
isFetching
} = this.props;
if (term && term !== prevProps.term) {
this.setState({
term,
isFetching: true
});
this.props.onMovieLookupChange(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.onMovieLookupChange(value);
} else {
this.props.onClearMovieLookup();
}
});
}
onClearMovieLookupPress = () => {
this.setState({ term: '' });
this.props.onClearMovieLookup();
}
//
// Render
render() {
const {
error,
items,
hasExistingMovies
} = this.props;
const term = this.state.term;
const isFetching = this.state.isFetching;
return (
<PageContent title={translate('AddNewMovie')}>
<PageContentBody>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon
name={icons.SEARCH}
size={20}
/>
</div>
<TextInput
className={styles.searchInput}
name="movieLookup"
value={term}
placeholder="eg. The Dark Knight, tmdb:155, imdb:tt0468569"
autoFocus={true}
onChange={this.onSearchInputChange}
/>
<Button
className={styles.clearLookupButton}
onPress={this.onClearMovieLookupPress}
>
<Icon
name={icons.REMOVE}
size={20}
/>
</Button>
</div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error ?
<div className={styles.message}>
<div className={styles.helpText}>
{translate('FailedLoadingSearchResults')}
</div>
<div>{getErrorMessage(error)}</div>
</div> : null
}
{
!isFetching && !error && !!items.length &&
<div className={styles.searchResults}>
{
items.map((item) => {
return (
<AddNewMovieSearchResultConnector
key={item.tmdbId}
{...item}
/>
);
})
}
</div>
}
{
!isFetching && !error && !items.length && !!term &&
<div className={styles.message}>
<div className={styles.noResults}>
{translate('CouldNotFindResults', [term])}
</div>
<div>
{translate('YouCanAlsoSearch')}
</div>
<div>
<Link to="https://github.com/Prowlarr/Prowlarr/wiki/FAQ#why-cant-i-add-a-new-movie-when-i-know-the-tmdb-id">
{translate('CantFindMovie')}
</Link>
</div>
</div>
}
{
term ?
null :
<div className={styles.message}>
<div className={styles.helpText}>
{translate('AddNewMessage')}
</div>
<div>
{translate('AddNewTmdbIdMessage')}
</div>
</div>
}
{
!term && !hasExistingMovies ?
<div className={styles.message}>
<div className={styles.noMoviesText}>
{translate('HaveNotAddedMovies')}
</div>
<div>
<Button
to="/add/import"
kind={kinds.PRIMARY}
>
{translate('ImportExistingMovies')}
</Button>
</div>
</div> :
null
}
<div />
</PageContentBody>
</PageContent>
);
}
}
AddNewMovie.propTypes = {
term: PropTypes.string,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
hasExistingMovies: PropTypes.bool.isRequired,
onMovieLookupChange: PropTypes.func.isRequired,
onClearMovieLookup: PropTypes.func.isRequired
};
export default AddNewMovie;

View file

@ -1,104 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import parseUrl from 'Utilities/String/parseUrl';
import AddNewMovie from './AddNewMovie';
function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
(state) => state.movies.items.length,
(state) => state.router.location,
(addMovie, existingMoviesCount, location) => {
const { params } = parseUrl(location.search);
return {
...addMovie,
term: params.term,
hasExistingMovies: existingMoviesCount > 0
};
}
);
}
const mapDispatchToProps = {
lookupMovie,
clearAddMovie,
fetchRootFolders
};
class AddNewMovieConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._movieLookupTimeout = null;
}
componentDidMount() {
this.props.fetchRootFolders();
}
componentWillUnmount() {
if (this._movieLookupTimeout) {
clearTimeout(this._movieLookupTimeout);
}
this.props.clearAddMovie();
}
//
// Listeners
onMovieLookupChange = (term) => {
if (this._movieLookupTimeout) {
clearTimeout(this._movieLookupTimeout);
}
if (term.trim() === '') {
this.props.clearAddMovie();
} else {
this._movieLookupTimeout = setTimeout(() => {
this.props.lookupMovie({ term });
}, 300);
}
}
onClearMovieLookup = () => {
this.props.clearAddMovie();
}
//
// Render
render() {
const {
term,
...otherProps
} = this.props;
return (
<AddNewMovie
term={term}
{...otherProps}
onMovieLookupChange={this.onMovieLookupChange}
onClearMovieLookup={this.onClearMovieLookup}
/>
);
}
}
AddNewMovieConnector.propTypes = {
term: PropTypes.string,
lookupMovie: PropTypes.func.isRequired,
clearAddMovie: PropTypes.func.isRequired,
fetchRootFolders: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);

View file

@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddNewMovieModalContentConnector from './AddNewMovieModalContentConnector';
function AddNewMovieModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddNewMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddNewMovieModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddNewMovieModal;

View file

@ -1,68 +0,0 @@
.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;
}
.labelIcon {
margin-left: 8px;
}
.searchForMissingMovieLabelContainer {
display: flex;
margin-top: 2px;
}
.searchForMissingMovieLabel {
margin-right: 8px;
font-weight: normal;
}
.searchForMissingMovieContainer {
composes: container from '~Components/Form/CheckInput.css';
flex: 0 1 0;
}
.searchForMissingMovieInput {
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';
}
@media only screen and (max-width: $breakpointSmall) {
.modalFooter {
display: block;
text-align: center;
}
.addButton {
margin-top: 10px;
}
}

View file

@ -1,217 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import SpinnerButton from 'Components/Link/SpinnerButton';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
import styles from './AddNewMovieModalContent.css';
class AddNewMovieModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
searchForMovie: false
};
}
//
// Listeners
onSearchForMissingMovieChange = ({ value }) => {
this.setState({ searchForMovie: value });
}
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
}
onAddMoviePress = () => {
this.props.onAddMoviePress(this.state.searchForMovie);
}
//
// Render
render() {
const {
title,
year,
overview,
images,
isAdding,
rootFolderPath,
monitor,
qualityProfileId,
minimumAvailability,
folder,
tags,
isSmallScreen,
isWindows,
onModalClose,
onInputChange
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{title}
{
!title.contains(year) && !!year &&
<span className={styles.year}>({year})</span>
}
</ModalHeader>
<ModalBody>
<div className={styles.container}>
{
!isSmallScreen &&
<div className={styles.poster}>
<MoviePoster
className={styles.poster}
images={images}
size={250}
/>
</div>
}
<div className={styles.info}>
<div className={styles.overview}>
{overview}
</div>
<Form>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
valueOptions={{
movieFolder: folder,
isWindows
}}
selectedValueOptions={{
movieFolder: folder,
isWindows
}}
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
onChange={onInputChange}
{...rootFolderPath}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('Monitor')}
</FormLabel>
<FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT}
name="monitor"
onChange={onInputChange}
{...monitor}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
onChange={onInputChange}
{...minimumAvailability}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
onChange={this.onQualityProfileIdChange}
{...qualityProfileId}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
onChange={onInputChange}
{...tags}
/>
</FormGroup>
</Form>
</div>
</div>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<label className={styles.searchForMissingMovieLabelContainer}>
<span className={styles.searchForMissingMovieLabel}>
{translate('StartSearchForMissingMovie')}
</span>
<CheckInput
containerClassName={styles.searchForMissingMovieContainer}
className={styles.searchForMissingMovieInput}
name="searchForMovie"
value={this.state.searchForMovie}
onChange={this.onSearchForMissingMovieChange}
/>
</label>
<SpinnerButton
className={styles.addButton}
kind={kinds.SUCCESS}
isSpinning={isAdding}
onPress={this.onAddMoviePress}
>
{translate('AddMovie')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
AddNewMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
year: PropTypes.number.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,
minimumAvailability: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
onAddMoviePress: PropTypes.func.isRequired
};
export default AddNewMovieModalContent;

View file

@ -1,103 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addMovie, setAddMovieDefault } from 'Store/Actions/addMovieActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import AddNewMovieModalContent from './AddNewMovieModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
createDimensionsSelector(),
createSystemStatusSelector(),
(addMovieState, dimensions, systemStatus) => {
const {
isAdding,
addError,
defaults
} = addMovieState;
const {
settings,
validationErrors,
validationWarnings
} = selectSettings(defaults, {}, addError);
return {
isAdding,
addError,
isSmallScreen: dimensions.isSmallScreen,
validationErrors,
validationWarnings,
isWindows: systemStatus.isWindows,
...settings
};
}
);
}
const mapDispatchToProps = {
setAddMovieDefault,
addMovie
};
class AddNewMovieModalContentConnector extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setAddMovieDefault({ [name]: value });
}
onAddMoviePress = (searchForMovie) => {
const {
tmdbId,
rootFolderPath,
monitor,
qualityProfileId,
minimumAvailability,
tags
} = this.props;
this.props.addMovie({
tmdbId,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
minimumAvailability: minimumAvailability.value,
tags: tags.value,
searchForMovie
});
}
//
// Render
render() {
return (
<AddNewMovieModalContent
{...this.props}
onInputChange={this.onInputChange}
onAddMoviePress={this.onAddMoviePress}
/>
);
}
}
AddNewMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired,
setAddMovieDefault: PropTypes.func.isRequired,
addMovie: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieModalContentConnector);

View file

@ -1,94 +0,0 @@
.searchResult {
position: relative;
margin: 20px 0;
padding: 20px;
width: 100%;
color: inherit;
}
.underlay {
@add-mixin cover;
background-color: $white;
transition: background 500ms;
&:hover {
background-color: #eaf2ff;
color: inherit;
text-decoration: none;
}
}
.overlay {
@add-mixin linkOverlay;
position: relative;
display: flex;
}
.poster {
flex: 0 0 170px;
margin-right: 20px;
height: 250px;
}
.content {
flex: 0 1 100%;
overflow: hidden;
}
.titleRow {
display: flex;
}
.titleContainer {
display: flex;
align-items: flex-end;
flex: 0 1 auto;
}
.title {
font-weight: 300;
font-size: 36px;
}
.year {
margin-left: 10px;
color: $disabledColor;
}
.icons {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 auto;
height: 55px;
}
.alreadyExistsIcon {
margin-left: 10px;
color: #37bc9b;
pointer-events: all;
}
.exclusionIcon {
margin-left: 10px;
color: $dangerColor;
pointer-events: all;
}
.overview {
margin-top: 20px;
}
.links {
margin-left: 8px;
pointer-events: all;
}
@media only screen and (max-width: $breakpointMedium) {
.titleRow {
justify-content: space-between;
overflow: hidden;
}
}

View file

@ -1,218 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
import AddNewMovieModal from './AddNewMovieModal';
import styles from './AddNewMovieSearchResult.css';
class AddNewMovieSearchResult extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isNewAddMovieModalOpen: false
};
}
componentDidUpdate(prevProps) {
if (!prevProps.isExistingMovie && this.props.isExistingMovie) {
this.onAddMovieModalClose();
}
}
//
// Listeners
onPress = () => {
this.setState({ isNewAddMovieModalOpen: true });
}
onAddMovieModalClose = () => {
this.setState({ isNewAddMovieModalOpen: false });
}
onExternalLinkPress = (event) => {
event.stopPropagation();
}
//
// Render
render() {
const {
tmdbId,
title,
titleSlug,
year,
studio,
status,
overview,
ratings,
folder,
images,
isExistingMovie,
isExclusionMovie,
isSmallScreen
} = this.props;
const {
isNewAddMovieModalOpen
} = this.state;
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
return (
<div className={styles.searchResult}>
<Link
className={styles.underlay}
{...linkProps}
/>
<div className={styles.overlay}>
{
isSmallScreen ?
null :
<MoviePoster
className={styles.poster}
images={images}
size={250}
overflow={true}
/>
}
<div className={styles.content}>
<div className={styles.titleRow}>
<div className={styles.titleContainer}>
<div className={styles.title}>
{title}
{
!title.contains(year) && !!year ?
<span className={styles.year}>
({year})
</span> :
null
}
</div>
</div>
<div className={styles.icons}>
{
isExistingMovie &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title={translate('AlreadyInYourLibrary')}
/>
}
{
isExclusionMovie &&
<Icon
className={styles.exclusionIcon}
name={icons.DANGER}
size={36}
title={translate('MovieIsOnImportExclusionList')}
/>
}
</div>
</div>
<div>
<Label size={sizes.LARGE}>
<HeartRating
rating={ratings.value}
iconSize={13}
/>
</Label>
{
!!studio &&
<Label size={sizes.LARGE}>
{studio}
</Label>
}
<Tooltip
anchor={
<Label
size={sizes.LARGE}
>
<Icon
name={icons.EXTERNAL_LINK}
size={13}
/>
<span className={styles.links}>
Links
</span>
</Label>
}
canFlip={true}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
{
status === 'ended' &&
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
Ended
</Label>
}
</div>
<div className={styles.overview}>
{overview}
</div>
</div>
</div>
<AddNewMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
</div>
);
}
}
AddNewMovieSearchResult.propTypes = {
tmdbId: PropTypes.number.isRequired,
imdbId: PropTypes.string,
youTubeTrailerId: PropTypes.string,
title: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
studio: PropTypes.string,
status: PropTypes.string.isRequired,
overview: PropTypes.string,
ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired
};
export default AddNewMovieSearchResult;

View file

@ -1,23 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
function createMapStateToProps() {
return createSelector(
createExistingMovieSelector(),
createExclusionMovieSelector(),
createDimensionsSelector(),
(isExistingMovie, isExclusionMovie, dimensions) => {
return {
isExistingMovie,
isExclusionMovie,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
export default connect(createMapStateToProps)(AddNewMovieSearchResult);

View file

@ -1,177 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import ImportMovieFooterConnector from './ImportMovieFooterConnector';
import ImportMovieTableConnector from './ImportMovieTableConnector';
class ImportMovie extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
contentBody: null
};
}
//
// Control
setScrollerRef = (ref) => {
this.setState({ scroller: 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());
}
//
// Render
render() {
const {
rootFolderId,
path,
rootFoldersFetching,
rootFoldersError,
rootFoldersPopulated,
unmappedFolders
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
scroller
} = this.state;
return (
<PageContent title={translate('ImportMovies')}>
<PageContentBody
registerScroller={this.setScrollerRef}
onScroll={this.onScroll}
>
{
rootFoldersFetching ? <LoadingIndicator /> : null
}
{
!rootFoldersFetching && !!rootFoldersError ?
<div>
{translate('UnableToLoadRootFolders')}
</div> :
null
}
{
!rootFoldersError &&
!rootFoldersFetching &&
rootFoldersPopulated &&
!unmappedFolders.length ?
<div>
All movies in {path} have been imported
</div> :
null
}
{
!rootFoldersError &&
!rootFoldersFetching &&
rootFoldersPopulated &&
!!unmappedFolders.length &&
scroller ?
<ImportMovieTableConnector
rootFolderId={rootFolderId}
unmappedFolders={unmappedFolders}
allSelected={allSelected}
allUnselected={allUnselected}
selectedState={selectedState}
scroller={scroller}
onSelectAllChange={this.onSelectAllChange}
onSelectedChange={this.onSelectedChange}
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
/> :
null
}
</PageContentBody>
{
!rootFoldersError &&
!rootFoldersFetching &&
!!unmappedFolders.length ?
<ImportMovieFooterConnector
selectedIds={this.getSelectedIds()}
onInputChange={this.onInputChange}
onImportPress={this.onImportPress}
/> :
null
}
</PageContent>
);
}
}
ImportMovie.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),
onInputChange: PropTypes.func.isRequired,
onImportPress: PropTypes.func.isRequired
};
ImportMovie.defaultProps = {
unmappedFolders: []
};
export default ImportMovie;

View file

@ -1,153 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
import { setAddMovieDefault } from 'Store/Actions/addMovieActions';
import { clearImportMovie, importMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import ImportMovie from './ImportMovie';
function createMapStateToProps() {
return createSelector(
(state, { match }) => match,
(state) => state.rootFolders,
(state) => state.addMovie,
(state) => state.importMovie,
(state) => state.settings.qualityProfiles,
(
match,
rootFolders,
addMovie,
importMovieState,
qualityProfiles
) => {
const {
isFetching: rootFoldersFetching,
isPopulated: rootFoldersPopulated,
error: rootFoldersError,
items
} = rootFolders;
const rootFolderId = parseInt(match.params.rootFolderId);
const result = {
rootFolderId,
rootFoldersFetching,
rootFoldersPopulated,
rootFoldersError,
qualityProfiles: qualityProfiles.items,
defaultQualityProfileId: addMovie.defaults.qualityProfileId
};
if (items.length) {
const rootFolder = _.find(items, { id: rootFolderId });
return {
...result,
...rootFolder,
items: importMovieState.items
};
}
return result;
}
);
}
const mapDispatchToProps = {
dispatchSetImportMovieValue: setImportMovieValue,
dispatchImportMovie: importMovie,
dispatchClearImportMovie: clearImportMovie,
dispatchFetchRootFolders: fetchRootFolders,
dispatchSetAddMovieDefault: setAddMovieDefault
};
class ImportMovieConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
rootFolderId,
qualityProfiles,
defaultQualityProfileId,
dispatchFetchRootFolders,
dispatchSetAddMovieDefault
} = this.props;
dispatchFetchRootFolders({ id: rootFolderId, timeout: false });
let setDefaults = false;
const setDefaultPayload = {};
if (
!defaultQualityProfileId ||
!qualityProfiles.some((p) => p.id === defaultQualityProfileId)
) {
setDefaults = true;
setDefaultPayload.qualityProfileId = qualityProfiles[0].id;
}
if (setDefaults) {
dispatchSetAddMovieDefault(setDefaultPayload);
}
}
componentWillUnmount() {
this.props.dispatchClearImportMovie();
}
//
// Listeners
onInputChange = (ids, name, value) => {
this.props.dispatchSetAddMovieDefault({ [name]: value });
ids.forEach((id) => {
this.props.dispatchSetImportMovieValue({
id,
[name]: value
});
});
}
onImportPress = (ids) => {
this.props.dispatchImportMovie({ ids });
}
//
// Render
render() {
return (
<ImportMovie
{...this.props}
onInputChange={this.onInputChange}
onImportPress={this.onImportPress}
/>
);
}
}
const routeMatchShape = createRouteMatchShape({
rootFolderId: PropTypes.string.isRequired
});
ImportMovieConnector.propTypes = {
match: routeMatchShape.isRequired,
rootFolderId: PropTypes.number.isRequired,
rootFoldersFetching: PropTypes.bool.isRequired,
rootFoldersPopulated: PropTypes.bool.isRequired,
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
defaultQualityProfileId: PropTypes.number.isRequired,
dispatchSetImportMovieValue: PropTypes.func.isRequired,
dispatchImportMovie: PropTypes.func.isRequired,
dispatchClearImportMovie: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchSetAddMovieDefault: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieConnector);

View file

@ -1,37 +0,0 @@
.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;
}
.loadingButton {
composes: importButton;
margin-left: 10px;
}
.loading {
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin: 0 10px 0 12px;
text-align: left;
}
.importError {
margin-left: 10px;
}

View file

@ -1,267 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
// import CheckInput from 'Components/Form/CheckInput';
import FormInputGroup from 'Components/Form/FormInputGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContentFooter from 'Components/Page/PageContentFooter';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieFooter.css';
const MIXED = 'mixed';
class ImportMovieFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
defaultMonitor,
defaultQualityProfileId,
defaultMinimumAvailability
} = props;
this.state = {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability
};
}
componentDidUpdate(prevProps, prevState) {
const {
defaultMonitor,
defaultQualityProfileId,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdMixed,
isMinimumAvailabilityMixed
} = this.props;
const {
monitor,
qualityProfileId,
minimumAvailability
} = 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 (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
newState.minimumAvailability = MIXED;
} else if (!isMinimumAvailabilityMixed && minimumAvailability !== defaultMinimumAvailability) {
newState.minimumAvailability = defaultMinimumAvailability;
}
if (!_.isEmpty(newState)) {
this.setState(newState);
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
this.props.onInputChange({ name, value });
}
//
// Render
render() {
const {
selectedCount,
isImporting,
isLookingUpMovie,
isMonitorMixed,
isQualityProfileIdMixed,
isMinimumAvailabilityMixed,
hasUnsearchedItems,
importError,
onImportPress,
onLookupPress,
onCancelLookupPress
} = this.props;
const {
monitor,
qualityProfileId,
minimumAvailability
} = this.state;
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('Monitor')}
</div>
<FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT}
name="monitor"
value={monitor}
isDisabled={!selectedCount}
includeMixed={isMonitorMixed}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('MinimumAvailability')}
</div>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
value={minimumAvailability}
isDisabled={!selectedCount}
includeMixed={isMinimumAvailabilityMixed}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('QualityProfile')}
</div>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
isDisabled={!selectedCount}
includeMixed={isQualityProfileIdMixed}
onChange={this.onInputChange}
/>
</div>
<div>
<div className={styles.label}>
&nbsp;
</div>
<div className={styles.importButtonContainer}>
<SpinnerButton
className={styles.importButton}
kind={kinds.PRIMARY}
isSpinning={isImporting}
isDisabled={!selectedCount || isLookingUpMovie}
onPress={onImportPress}
>
{translate('Import')} {selectedCount} {selectedCount > 1 ? translate('Movies') : translate('Movie')}
</SpinnerButton>
{
isLookingUpMovie ?
<Button
className={styles.loadingButton}
kind={kinds.WARNING}
onPress={onCancelLookupPress}
>
{translate('CancelProcessing')}
</Button> :
null
}
{
hasUnsearchedItems ?
<Button
className={styles.loadingButton}
kind={kinds.SUCCESS}
onPress={onLookupPress}
>
{translate('StartProcessing')}
</Button> :
null
}
{
isLookingUpMovie ?
<LoadingIndicator
className={styles.loading}
size={24}
/> :
null
}
{
isLookingUpMovie ?
translate('ProcessingFolders') :
null
}
{
importError ?
<Popover
anchor={
<Icon
className={styles.importError}
name={icons.WARNING}
kind={kinds.WARNING}
/>
}
title={translate('ImportErrors')}
body={
<ul>
{
importError.responseJSON.map((error, index) => {
return (
<li key={index}>
{error.errorMessage}
</li>
);
})
}
</ul>
}
position={tooltipPositions.RIGHT}
/> :
null
}
</div>
</div>
</PageContentFooter>
);
}
}
ImportMovieFooter.propTypes = {
selectedCount: PropTypes.number.isRequired,
isImporting: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired,
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number,
defaultMinimumAvailability: PropTypes.string,
isMonitorMixed: PropTypes.bool.isRequired,
isQualityProfileIdMixed: PropTypes.bool.isRequired,
isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
hasUnsearchedItems: PropTypes.bool.isRequired,
importError: PropTypes.object,
onInputChange: PropTypes.func.isRequired,
onImportPress: PropTypes.func.isRequired,
onLookupPress: PropTypes.func.isRequired,
onCancelLookupPress: PropTypes.func.isRequired
};
export default ImportMovieFooter;

View file

@ -1,59 +0,0 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { cancelLookupMovie, lookupUnsearchedMovies } from 'Store/Actions/importMovieActions';
import ImportMovieFooter from './ImportMovieFooter';
function isMixed(items, selectedIds, defaultValue, key) {
return _.some(items, (movie) => {
return selectedIds.indexOf(movie.id) > -1 && movie[key] !== defaultValue;
});
}
function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
(state) => state.importMovie,
(state, { selectedIds }) => selectedIds,
(addMovie, importMovie, selectedIds) => {
const {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability
} = addMovie.defaults;
const {
isLookingUpMovie,
isImporting,
items,
importError
} = importMovie;
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
return {
selectedCount: selectedIds.length,
isLookingUpMovie,
isImporting,
defaultMonitor,
defaultQualityProfileId,
defaultMinimumAvailability,
isMonitorMixed,
isQualityProfileIdMixed,
isMinimumAvailabilityMixed,
importError,
hasUnsearchedItems
};
}
);
}
const mapDispatchToProps = {
onLookupPress: lookupUnsearchedMovies,
onCancelLookupPress: cancelLookupMovie
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieFooter);

View file

@ -1,31 +0,0 @@
.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;
}
.minimumAvailability,
.qualityProfile {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 250px;
min-width: 170px;
}
.movie {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 400px;
min-width: 300px;
}
.detailsIcon {
margin-left: 8px;
}

View file

@ -1,68 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieHeader.css';
function ImportMovieHeader(props) {
const {
allSelected,
allUnselected,
onSelectAllChange
} = props;
return (
<VirtualTableHeader>
<VirtualTableSelectAllHeaderCell
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
<VirtualTableHeaderCell
className={styles.folder}
name="folder"
>
{translate('Folder')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.monitor}
name="monitor"
>
{translate('Monitor')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.minimumAvailability}
name="minimumAvailability"
>
{translate('MinAvailability')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.qualityProfile}
name="qualityProfileId"
>
{translate('QualityProfile')}
</VirtualTableHeaderCell>
<VirtualTableHeaderCell
className={styles.movie}
name="movie"
>
{translate('Movie')}
</VirtualTableHeaderCell>
</VirtualTableHeader>
);
}
ImportMovieHeader.propTypes = {
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired
};
export default ImportMovieHeader;

View file

@ -1,32 +0,0 @@
.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;
}
.minimumAvailability,
.qualityProfile {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 250px;
min-width: 170px;
}
.movie {
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 400px;
min-width: 300px;
}

View file

@ -1,92 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FormInputGroup from 'Components/Form/FormInputGroup';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import { inputTypes } from 'Helpers/Props';
import ImportMovieSelectMovieConnector from './SelectMovie/ImportMovieSelectMovieConnector';
import styles from './ImportMovieRow.css';
function ImportMovieRow(props) {
const {
id,
monitor,
qualityProfileId,
minimumAvailability,
selectedMovie,
isExistingMovie,
isSelected,
onSelectedChange,
onInputChange
} = props;
return (
<>
<VirtualTableSelectCell
inputClassName={styles.selectInput}
id={id}
isSelected={isSelected}
isDisabled={!selectedMovie || isExistingMovie}
onSelectedChange={onSelectedChange}
/>
<VirtualTableRowCell className={styles.folder}>
{id}
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.monitor}>
<FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT}
name="monitor"
value={monitor}
onChange={onInputChange}
/>
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.minimumAvailability}>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
value={minimumAvailability}
onChange={onInputChange}
/>
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.qualityProfile}>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
value={qualityProfileId}
onChange={onInputChange}
/>
</VirtualTableRowCell>
<VirtualTableRowCell className={styles.movie}>
<ImportMovieSelectMovieConnector
id={id}
isExistingMovie={isExistingMovie}
/>
</VirtualTableRowCell>
</>
);
}
ImportMovieRow.propTypes = {
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired,
selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
queued: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired
};
ImportMovieRow.defaultsProps = {
items: []
};
export default ImportMovieRow;

View file

@ -1,87 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { queueLookupMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import ImportMovieRow from './ImportMovieRow';
function createImportMovieItemSelector() {
return createSelector(
(state, { id }) => id,
(state) => state.importMovie.items,
(id, items) => {
return _.find(items, { id }) || {};
}
);
}
function createMapStateToProps() {
return createSelector(
createImportMovieItemSelector(),
createAllMoviesSelector(),
(item, movies) => {
const selectedMovie = item && item.selectedMovie;
const isExistingMovie = !!selectedMovie && _.some(movies, { tmdbId: selectedMovie.tmdbId });
return {
...item,
isExistingMovie
};
}
);
}
const mapDispatchToProps = {
queueLookupMovie,
setImportMovieValue
};
class ImportMovieRowConnector extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setImportMovieValue({
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
} = this.props;
if (!items || !monitor) {
return null;
}
return (
<ImportMovieRow
{...this.props}
onInputChange={this.onInputChange}
onMovieSelect={this.onMovieSelect}
/>
);
}
}
ImportMovieRowConnector.propTypes = {
rootFolderId: PropTypes.number.isRequired,
id: PropTypes.string.isRequired,
monitor: PropTypes.string,
items: PropTypes.arrayOf(PropTypes.object),
queueLookupMovie: PropTypes.func.isRequired,
setImportMovieValue: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieRowConnector);

View file

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

View file

@ -1,185 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import ImportMovieHeader from './ImportMovieHeader';
import ImportMovieRowConnector from './ImportMovieRowConnector';
class ImportMovieTable extends Component {
//
// Lifecycle
componentDidMount() {
const {
unmappedFolders,
defaultMonitor,
defaultQualityProfileId,
defaultMinimumAvailability,
onMovieLookup,
onSetImportMovieValue
} = this.props;
const values = {
monitor: defaultMonitor,
qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability
};
unmappedFolders.forEach((unmappedFolder) => {
const id = unmappedFolder.name;
onMovieLookup(id, unmappedFolder.path);
onSetImportMovieValue({
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 selectedMovie = item.selectedMovie;
const isSelected = selectedState[id];
const isExistingMovie = !!selectedMovie &&
_.some(prevProps.allMovies, { tmdbId: selectedMovie.tmdbId });
// Props doesn't have a selected movie or
// the selected movie is an existing movie.
if ((!selectedMovie && prevItem.selectedMovie) || (isExistingMovie && !prevItem.selectedMovie)) {
onSelectedChange({ id, value: false });
return;
}
// State is selected, but a movie isn't selected or
// the selected movie is an existing movie.
if (isSelected && (!selectedMovie || isExistingMovie)) {
onSelectedChange({ id, value: false });
return;
}
// A movie is being selected that wasn't previously selected.
if (selectedMovie && selectedMovie !== prevItem.selectedMovie) {
onSelectedChange({ id, value: true });
return;
}
});
}
//
// Control
rowRenderer = ({ key, rowIndex, style }) => {
const {
rootFolderId,
items,
selectedState,
onSelectedChange
} = this.props;
const item = items[rowIndex];
return (
<VirtualTableRow
key={key}
style={style}
>
<ImportMovieRowConnector
key={item.id}
rootFolderId={rootFolderId}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
</VirtualTableRow>
);
}
//
// Render
render() {
const {
items,
allSelected,
allUnselected,
isSmallScreen,
scroller,
selectedState,
onSelectAllChange
} = this.props;
if (!items.length) {
return null;
}
return (
<VirtualTable
items={items}
isSmallScreen={isSmallScreen}
scroller={scroller}
rowHeight={52}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
<ImportMovieHeader
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
}
selectedState={selectedState}
/>
);
}
}
ImportMovieTable.propTypes = {
rootFolderId: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object),
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileId: PropTypes.number,
defaultMinimumAvailability: PropTypes.string,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
allMovies: PropTypes.arrayOf(PropTypes.object),
scroller: PropTypes.instanceOf(Element).isRequired,
onSelectAllChange: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemoveSelectedStateItem: PropTypes.func.isRequired,
onMovieLookup: PropTypes.func.isRequired,
onSetImportMovieValue: PropTypes.func.isRequired
};
export default ImportMovieTable;

View file

@ -1,42 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { queueLookupMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import ImportMovieTable from './ImportMovieTable';
function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
(state) => state.importMovie,
(state) => state.app.dimensions,
createAllMoviesSelector(),
(addMovie, importMovie, dimensions, allMovies) => {
return {
defaultMonitor: addMovie.defaults.monitor,
defaultQualityProfileId: addMovie.defaults.qualityProfileId,
defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
items: importMovie.items,
isSmallScreen: dimensions.isSmallScreen,
allMovies
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onMovieLookup(name, path) {
dispatch(queueLookupMovie({
name,
path,
term: name
}));
},
onSetImportMovieValue(values) {
dispatch(setImportMovieValue(values));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(ImportMovieTable);

View file

@ -1,8 +0,0 @@
.movie {
padding: 10px 20px;
width: 100%;
&:hover {
background-color: $menuItemHoverBackgroundColor;
}
}

View file

@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import ImportMovieTitle from './ImportMovieTitle';
import styles from './ImportMovieSearchResult.css';
class ImportMovieSearchResult extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.tmdbId);
}
//
// Render
render() {
const {
title,
year,
studio,
isExistingMovie
} = this.props;
return (
<Link
className={styles.movie}
onPress={this.onPress}
>
<ImportMovieTitle
title={title}
year={year}
network={studio}
isExistingMovie={isExistingMovie}
/>
</Link>
);
}
}
ImportMovieSearchResult.propTypes = {
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
studio: PropTypes.string,
isExistingMovie: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired
};
export default ImportMovieSearchResult;

View file

@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
import ImportMovieSearchResult from './ImportMovieSearchResult';
function createMapStateToProps() {
return createSelector(
createExistingMovieSelector(),
(isExistingMovie) => {
return {
isExistingMovie
};
}
);
}
export default connect(createMapStateToProps)(ImportMovieSearchResult);

View file

@ -1,66 +0,0 @@
.button {
composes: link from '~Components/Link/Link.css';
display: flex;
align-items: center;
justify-content: space-between;
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 {
right: 16px;
}
.contentContainer {
z-index: $popperZIndex;
margin-top: 4px;
/* 400px container witdh with 8px padding on each side */
width: 384px;
}
.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: input from '~Components/Form/TextInput.css';
border-radius: 0;
}

View file

@ -1,303 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Manager, Popper, Reference } from 'react-popper';
import FormInputButton from 'Components/Form/FormInputButton';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import translate from 'Utilities/String/translate';
import ImportMovieSearchResultConnector from './ImportMovieSearchResultConnector';
import ImportMovieTitle from './ImportMovieTitle';
import styles from './ImportMovieSelectMovie.css';
class ImportMovieSelectMovie extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._movieLookupTimeout = null;
this._scheduleUpdate = null;
this._buttonId = getUniqueElememtId();
this._contentId = getUniqueElememtId();
this.state = {
term: props.id,
isOpen: false
};
}
componentDidUpdate() {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
}
//
// Control
_addListener() {
window.addEventListener('click', this.onWindowClick);
}
_removeListener() {
window.removeEventListener('click', this.onWindowClick);
}
//
// Listeners
onWindowClick = (event) => {
const button = document.getElementById(this._buttonId);
const content = document.getElementById(this._contentId);
if (!button || !content) {
return;
}
if (
!button.contains(event.target) &&
!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._movieLookupTimeout) {
clearTimeout(this._movieLookupTimeout);
}
this.setState({ term: value }, () => {
this._movieLookupTimeout = setTimeout(() => {
this.props.onSearchInputChange(value);
}, 200);
});
}
onRefreshPress = () => {
this.props.onSearchInputChange(this.state.term);
}
onMovieSelect = (tmdbId) => {
this.setState({ isOpen: false });
this.props.onMovieSelect(tmdbId);
}
//
// Render
render() {
const {
selectedMovie,
isExistingMovie,
isFetching,
isPopulated,
error,
items,
isQueued,
isLookingUpMovie
} = this.props;
const errorMessage = error &&
error.responseJSON &&
error.responseJSON.message;
return (
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._buttonId}
>
<Link
ref={ref}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpMovie && isQueued && !isPopulated ?
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
}
{
isPopulated && selectedMovie && isExistingMovie ?
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/> :
null
}
{
isPopulated && selectedMovie ?
<ImportMovieTitle
title={selectedMovie.title}
year={selectedMovie.year}
studio={selectedMovie.studio}
isExistingMovie={isExistingMovie}
/> :
null
}
{
isPopulated && !selectedMovie ?
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
{translate('NoMatchFound')}
</div> :
null
}
{
!isFetching && !!error ?
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
{translate('SearchFailedPleaseTryAgainLater')}
</div> :
null
}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</div>
)}
</Reference>
<Portal>
<Popper
placement="bottom"
modifiers={{
preventOverflow: {
boundariesElement: 'viewport'
}
}}
>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
ref={ref}
id={this._contentId}
className={styles.contentContainer}
style={style}
>
{
this.state.isOpen ?
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
<div className={styles.results}>
{
items.map((item) => {
return (
<ImportMovieSearchResultConnector
key={item.tvdbId}
tmdbId={item.tmdbId}
title={item.title}
year={item.year}
studio={item.studio}
onPress={this.onMovieSelect}
/>
);
})
}
</div>
</div> :
null
}
</div>
);
}}
</Popper>
</Portal>
</Manager>
);
}
}
ImportMovieSelectMovie.propTypes = {
id: PropTypes.string.isRequired,
selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isQueued: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired,
onSearchInputChange: PropTypes.func.isRequired,
onMovieSelect: PropTypes.func.isRequired
};
ImportMovieSelectMovie.defaultProps = {
isFetching: true,
isPopulated: false,
items: [],
isQueued: true
};
export default ImportMovieSelectMovie;

View file

@ -1,76 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { queueLookupMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
import createImportMovieItemSelector from 'Store/Selectors/createImportMovieItemSelector';
import ImportMovieSelectMovie from './ImportMovieSelectMovie';
function createMapStateToProps() {
return createSelector(
(state) => state.importMovie.isLookingUpMovie,
createImportMovieItemSelector(),
(isLookingUpMovie, item) => {
return {
isLookingUpMovie,
...item
};
}
);
}
const mapDispatchToProps = {
queueLookupMovie,
setImportMovieValue
};
class ImportMovieSelectMovieConnector extends Component {
//
// Listeners
onSearchInputChange = (term) => {
this.props.queueLookupMovie({
name: this.props.id,
term,
topOfQueue: true
});
}
onMovieSelect = (tmdbId) => {
const {
id,
items
} = this.props;
this.props.setImportMovieValue({
id,
selectedMovie: _.find(items, { tmdbId })
});
}
//
// Render
render() {
return (
<ImportMovieSelectMovie
{...this.props}
onSearchInputChange={this.onSearchInputChange}
onMovieSelect={this.onMovieSelect}
/>
);
}
}
ImportMovieSelectMovieConnector.propTypes = {
id: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object),
selectedMovie: PropTypes.object,
isSelected: PropTypes.bool,
queueLookupMovie: PropTypes.func.isRequired,
setImportMovieValue: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieSelectMovieConnector);

View file

@ -1,17 +0,0 @@
.titleContainer {
display: flex;
align-items: center;
}
.title {
margin-right: 5px;
}
.year {
margin-left: 5px;
color: $disabledColor;
}
.existing {
margin-left: 5px;
}

View file

@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieTitle.css';
function ImportMovieTitle(props) {
const {
title,
year,
studio,
isExistingMovie
} = props;
return (
<div className={styles.titleContainer}>
<div className={styles.title}>
{title}
{
!title.contains(year) &&
<span className={styles.year}>({year})</span>
}
</div>
{
!!studio &&
<Label>{studio}</Label>
}
{
isExistingMovie &&
<Label
kind={kinds.WARNING}
>
{translate('Existing')}
</Label>
}
</div>
);
}
ImportMovieTitle.propTypes = {
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
studio: PropTypes.string,
isExistingMovie: PropTypes.bool.isRequired
};
export default ImportMovieTitle;

View file

@ -1,30 +0,0 @@
import React, { Component } from 'react';
import { Route } from 'react-router-dom';
import ImportMovieConnector from 'AddMovie/ImportMovie/Import/ImportMovieConnector';
import ImportMovieSelectFolderConnector from 'AddMovie/ImportMovie/SelectFolder/ImportMovieSelectFolderConnector';
import Switch from 'Components/Router/Switch';
class ImportMovies extends Component {
//
// Render
render() {
return (
<Switch>
<Route
exact={true}
path="/add/import"
component={ImportMovieSelectFolderConnector}
/>
<Route
path="/add/import/:rootFolderId"
component={ImportMovieConnector}
/>
</Switch>
);
}
}
export default ImportMovies;

View file

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

View file

@ -1,66 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './ImportMovieRootFolderRow.css';
function ImportMovieRootFolderRow(props) {
const {
id,
path,
freeSpace,
unmappedFolders,
onDeletePress
} = props;
const unmappedFoldersCount = unmappedFolders.length || '-';
return (
<TableRow>
<TableRowCell>
<Link
className={styles.link}
to={`/add/import/${id}`}
>
{path}
</Link>
</TableRowCell>
<TableRowCell className={styles.freeSpace}>
{formatBytes(freeSpace) || '-'}
</TableRowCell>
<TableRowCell className={styles.unmappedFolders}>
{unmappedFoldersCount}
</TableRowCell>
<TableRowCell className={styles.actions}>
<IconButton
title={translate('RemoveRootFolder')}
name={icons.REMOVE}
onPress={onDeletePress}
/>
</TableRowCell>
</TableRow>
);
}
ImportMovieRootFolderRow.propTypes = {
id: PropTypes.number.isRequired,
path: PropTypes.string.isRequired,
freeSpace: PropTypes.number.isRequired,
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
onDeletePress: PropTypes.func.isRequired
};
ImportMovieRootFolderRow.defaultProps = {
freeSpace: 0,
unmappedFolders: []
};
export default ImportMovieRootFolderRow;

View file

@ -1,48 +0,0 @@
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 ImportMovieRootFolderRow from './ImportMovieRootFolderRow';
function createMapStateToProps() {
return createSelector(
() => {
return {
};
}
);
}
const mapDispatchToProps = {
deleteRootFolder
};
class ImportMovieRootFolderRowConnector extends Component {
//
// Listeners
onDeletePress = () => {
this.props.deleteRootFolder({ id: this.props.id });
}
//
// Render
render() {
return (
<ImportMovieRootFolderRow
{...this.props}
onDeletePress={this.onDeletePress}
/>
);
}
}
ImportMovieRootFolderRowConnector.propTypes = {
id: PropTypes.number.isRequired,
deleteRootFolder: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieRootFolderRowConnector);

View file

@ -1,32 +0,0 @@
.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;
}

View file

@ -1,187 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import ImportMovieRootFolderRowConnector from './ImportMovieRootFolderRowConnector';
import styles from './ImportMovieSelectFolder.css';
const rootFolderColumns = [
{
name: 'path',
label: translate('Path'),
isVisible: true
},
{
name: 'freeSpace',
label: translate('FreeSpace'),
isVisible: true
},
{
name: 'unmappedFolders',
label: translate('UnmappedFolders'),
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class ImportMovieSelectFolder 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 {
isWindows,
isFetching,
isPopulated,
error,
items
} = this.props;
return (
<PageContent title={translate('ImportMovies')}>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadRootFolders')}
</div>
}
{
!error && isPopulated &&
<div>
<div className={styles.header}>
{translate('ImportHeader')}
</div>
<div className={styles.tips}>
{translate('ImportTipsMessage')}
<ul>
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportIncludeQuality', ['<code>movie.2008.bluray.mkv</code>']) }} />
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportRootPath', [`<code>${isWindows ? 'C:\\movies' : '/movies'}</code>`, `<code>${isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}</code>`]) }} />
</ul>
</div>
{
items.length > 0 ?
<div className={styles.recentFolders}>
<FieldSet legend={translate('RecentFolders')}>
<Table
columns={rootFolderColumns}
>
<TableBody>
{
items.map((rootFolder) => {
return (
<ImportMovieRootFolderRowConnector
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})
}
</TableBody>
</Table>
</FieldSet>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('ChooseAnotherFolder')}
</Button>
</div> :
<div className={styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('StartImport')}
</Button>
</div>
}
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={this.onNewRootFolderSelect}
onModalClose={this.onAddRootFolderModalClose}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
ImportMovieSelectFolder.propTypes = {
isWindows: PropTypes.bool.isRequired,
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 ImportMovieSelectFolder;

View file

@ -1,91 +0,0 @@
import { push } from 'connected-react-router';
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
createSystemStatusSelector(),
(rootFolders, systemStatus) => {
return {
...rootFolders,
isWindows: systemStatus.isWindows
};
}
);
}
const mapDispatchToProps = {
fetchRootFolders,
addRootFolder,
deleteRootFolder,
push
};
class ImportMovieSelectFolderConnector 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.Prowlarr.urlBase}/add/import/${newRootFolders[0].id}`);
}
}
}
//
// Listeners
onNewRootFolderSelect = (path) => {
this.props.addRootFolder({ path });
}
onDeleteRootFolderPress = (id) => {
this.props.deleteRootFolder({ id });
}
//
// Render
render() {
return (
<ImportMovieSelectFolder
{...this.props}
onNewRootFolderSelect={this.onNewRootFolderSelect}
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
/>
);
}
}
ImportMovieSelectFolderConnector.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)(ImportMovieSelectFolderConnector);

View file

@ -1,14 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Redirect, Route } from 'react-router-dom'; import { Redirect, Route } from 'react-router-dom';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import NotFound from 'Components/NotFound'; import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch'; import Switch from 'Components/Router/Switch';
import MovieIndexConnector from 'Movie/Index/MovieIndexConnector'; import HistoryConnector from 'History/HistoryConnector';
import MovieIndexConnector from 'Indexer/Index/MovieIndexConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector'; import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
import NotificationSettings from 'Settings/Notifications/NotificationSettings'; import NotificationSettings from 'Settings/Notifications/NotificationSettings';
@ -58,13 +54,17 @@ function AppRoutes(props) {
} }
<Route <Route
path="/add/new" path="/indexers/stats"
component={AddNewMovieConnector} component={MovieIndexConnector}
/> />
{/*
Search
*/}
<Route <Route
path="/add/import" path="/search"
component={ImportMovies} component={MovieIndexConnector}
/> />
{/* {/*
@ -72,20 +72,10 @@ function AppRoutes(props) {
*/} */}
<Route <Route
path="/activity/history" path="/history"
component={HistoryConnector} component={HistoryConnector}
/> />
<Route
path="/activity/queue"
component={QueueConnector}
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/* {/*
Settings Settings
*/} */}

View file

@ -132,7 +132,7 @@ CircularProgressBar.defaultProps = {
containerClassName: styles.circularProgressBarContainer, containerClassName: styles.circularProgressBarContainer,
size: 60, size: 60,
strokeWidth: 5, strokeWidth: 5,
strokeColor: colors.radarrYellow, strokeColor: colors.prowlarrYellow,
showProgressText: false showProgressText: false
}; };

View file

@ -9,8 +9,6 @@ import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MovieStatusFilterBuilderRowValue from './MovieStatusFilterBuilderRowValue'; import MovieStatusFilterBuilderRowValue from './MovieStatusFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css'; import styles from './FilterBuilderRow.css';
@ -62,12 +60,6 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.PROTOCOL: case filterBuilderValueTypes.PROTOCOL:
return ProtocolFilterBuilderRowValue; return ProtocolFilterBuilderRowValue;
case filterBuilderValueTypes.QUALITY:
return QualityFilterBuilderRowValueConnector;
case filterBuilderValueTypes.QUALITY_PROFILE:
return QualityProfileFilterBuilderRowValueConnector;
case filterBuilderValueTypes.MOVIE_STATUS: case filterBuilderValueTypes.MOVIE_STATUS:
return MovieStatusFilterBuilderRowValue; return MovieStatusFilterBuilderRowValue;

View file

@ -1,75 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import getQualities from 'Utilities/Quality/getQualities';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles,
(qualityProfiles) => {
const {
isSchemaFetching: isFetching,
isSchemaPopulated: isPopulated,
schemaError: error,
schema
} = qualityProfiles;
const tagList = getQualities(schema.items);
return {
isFetching,
isPopulated,
error,
tagList
};
}
);
}
const mapDispatchToProps = {
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema
};
class QualityFilterBuilderRowValueConnector extends Component {
//
// Lifecycle
componentDidMount = () => {
if (!this.props.isPopulated) {
this.props.dispatchFetchQualityProfileSchema();
}
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
...otherProps
} = this.props;
return (
<FilterBuilderRowValue
{...otherProps}
/>
);
}
}
QualityFilterBuilderRowValueConnector.propTypes = {
tagList: PropTypes.arrayOf(PropTypes.shape(tagShape)).isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(QualityFilterBuilderRowValueConnector);

View file

@ -1,28 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles,
(qualityProfiles) => {
const tagList = qualityProfiles.items.map((qualityProfile) => {
const {
id,
name
} = qualityProfile;
return {
id,
name
};
});
return {
tagList
};
}
);
}
export default connect(createMapStateToProps)(FilterBuilderRowValue);

View file

@ -17,8 +17,6 @@ import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector'; import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector'; import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import TagInputConnector from './TagInputConnector'; import TagInputConnector from './TagInputConnector';
import TagSelectInputConnector from './TagSelectInputConnector'; import TagSelectInputConnector from './TagSelectInputConnector';
import TextArea from './TextArea'; import TextArea from './TextArea';
@ -58,15 +56,9 @@ function getComponent(type) {
case inputTypes.PATH: case inputTypes.PATH:
return PathInputConnector; return PathInputConnector;
case inputTypes.QUALITY_PROFILE_SELECT:
return QualityProfileSelectInputConnector;
case inputTypes.MOVIE_MONITORED_SELECT: case inputTypes.MOVIE_MONITORED_SELECT:
return MovieMonitoredSelectInput; return MovieMonitoredSelectInput;
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;
case inputTypes.INDEXER_FLAGS_SELECT: case inputTypes.INDEXER_FLAGS_SELECT:
return IndexerFlagsSelectInputConnector; return IndexerFlagsSelectInputConnector;

View file

@ -1,99 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
createSortedSectionSelector('settings.qualityProfiles', sortByName),
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(qualityProfiles, includeNoChange, includeMixed) => {
const values = _.map(qualityProfiles.items, (qualityProfile) => {
return {
key: qualityProfile.id,
value: qualityProfile.name
};
});
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
disabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
});
}
return {
values
};
}
);
}
class QualityProfileSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
name,
value,
values
} = this.props;
if (!value || !values.some((v) => v.key === value) ) {
const firstValue = _.find(values, (option) => !isNaN(parseInt(option.key)));
if (firstValue) {
this.onChange({ name, value: firstValue.key });
}
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
}
//
// Render
render() {
return (
<SelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
QualityProfileSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};
QualityProfileSelectInputConnector.defaultProps = {
includeNoChange: false
};
export default connect(createMapStateToProps)(QualityProfileSelectInputConnector);

View file

@ -1,109 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import EnhancedSelectInput from './EnhancedSelectInput';
import RootFolderSelectInputOption from './RootFolderSelectInputOption';
import RootFolderSelectInputSelectedValue from './RootFolderSelectInputSelectedValue';
class RootFolderSelectInput extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isAddNewRootFolderModalOpen: false,
newRootFolderPath: ''
};
}
componentDidUpdate(prevProps) {
const {
name,
isSaving,
saveError,
onChange
} = this.props;
const newRootFolderPath = this.state.newRootFolderPath;
if (
prevProps.isSaving &&
!isSaving &&
!saveError &&
newRootFolderPath
) {
onChange({ name, value: newRootFolderPath });
this.setState({ newRootFolderPath: '' });
}
}
//
// Listeners
onChange = ({ name, value }) => {
if (value === 'addNew') {
this.setState({ isAddNewRootFolderModalOpen: true });
} else {
this.props.onChange({ name, value });
}
}
onNewRootFolderSelect = ({ value }) => {
this.setState({ newRootFolderPath: value }, () => {
this.props.onNewRootFolderSelect(value);
});
}
onAddRootFolderModalClose = () => {
this.setState({ isAddNewRootFolderModalOpen: false });
}
//
// Render
render() {
const {
includeNoChange,
onNewRootFolderSelect,
...otherProps
} = this.props;
return (
<div>
<EnhancedSelectInput
{...otherProps}
selectedValueComponent={RootFolderSelectInputSelectedValue}
optionComponent={RootFolderSelectInputOption}
onChange={this.onChange}
/>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
value=""
onChange={this.onNewRootFolderSelect}
onModalClose={this.onAddRootFolderModalClose}
/>
</div>
);
}
}
RootFolderSelectInput.propTypes = {
name: PropTypes.string.isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired
};
RootFolderSelectInput.defaultProps = {
includeNoChange: false
};
export default RootFolderSelectInput;

View file

@ -1,157 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addRootFolder } from 'Store/Actions/rootFolderActions';
import RootFolderSelectInput from './RootFolderSelectInput';
const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
(state, { includeNoChange }) => includeNoChange,
(rootFolders, includeNoChange) => {
const values = rootFolders.items.map((rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace
};
});
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
isDisabled: true
});
}
if (!values.length) {
values.push({
key: '',
value: '',
isDisabled: true,
isHidden: true
});
}
values.push({
key: ADD_NEW_KEY,
value: 'Add a new path'
});
return {
values,
isSaving: rootFolders.isSaving,
saveError: rootFolders.saveError
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchAddRootFolder(path) {
dispatch(addRootFolder({ path }));
}
};
}
class RootFolderSelectInputConnector extends Component {
//
// Lifecycle
UNSAFE_componentWillMount() {
const {
value,
values,
onChange
} = this.props;
if (value == null && values[0].key === '') {
onChange({ name, value: '' });
}
}
componentDidMount() {
const {
name,
value,
values,
onChange
} = this.props;
if (!value || !values.some((v) => v.key === value) || value === ADD_NEW_KEY) {
const defaultValue = values[0];
if (defaultValue.key === ADD_NEW_KEY) {
onChange({ name, value: '' });
} else {
onChange({ name, value: defaultValue.key });
}
}
}
componentDidUpdate(prevProps) {
const {
name,
value,
values,
onChange
} = this.props;
if (prevProps.values === values) {
return;
}
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
const defaultValue = values[0];
if (defaultValue.key !== ADD_NEW_KEY) {
onChange({ name, value: defaultValue.key });
}
}
}
//
// Listeners
onNewRootFolderSelect = (path) => {
this.props.dispatchAddRootFolder(path);
}
//
// Render
render() {
const {
dispatchAddRootFolder,
...otherProps
} = this.props;
return (
<RootFolderSelectInput
{...otherProps}
onNewRootFolderSelect={this.onNewRootFolderSelect}
/>
);
}
}
RootFolderSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeNoChange: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchAddRootFolder: PropTypes.func.isRequired
};
RootFolderSelectInputConnector.defaultProps = {
includeNoChange: false
};
export default connect(createMapStateToProps, createMapDispatchToProps)(RootFolderSelectInputConnector);

View file

@ -1,31 +0,0 @@
.optionText {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 0;
&.isMobile {
display: block;
.freeSpace {
margin-left: 0;
}
}
}
.value {
display: flex;
max-width: 500px;
}
.movieFolder {
@add-mixin truncate;
color: $disabledColor;
}
.freeSpace {
margin-left: 15px;
color: $darkGray;
font-size: $smallFontSize;
}

View file

@ -1,65 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
import styles from './RootFolderSelectInputOption.css';
function RootFolderSelectInputOption(props) {
const {
id,
value,
freeSpace,
movieFolder,
isMobile,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
return (
<EnhancedSelectInputOption
id={id}
isMobile={isMobile}
{...otherProps}
>
<div className={classNames(
styles.optionText,
isMobile && styles.isMobile
)}
>
<div className={styles.value}>
{value}
{
movieFolder && id !== 'addNew' ?
<div className={styles.movieFolder}>
{slashCharacter}
{movieFolder}
</div> :
null
}
</div>
{
freeSpace != null &&
<div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free
</div>
}
</div>
</EnhancedSelectInputOption>
);
}
RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
freeSpace: PropTypes.number,
movieFolder: PropTypes.string,
isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool
};
export default RootFolderSelectInputOption;

View file

@ -1,32 +0,0 @@
.selectedValue {
composes: selectedValue from '~./EnhancedSelectInputSelectedValue.css';
display: flex;
align-items: center;
justify-content: space-between;
overflow: hidden;
}
.pathContainer {
@add-mixin truncate;
display: flex;
flex: 1 0 0;
}
.path {
flex: 0 1 auto;
}
.movieFolder {
@add-mixin truncate;
flex: 0 1 auto;
color: $disabledColor;
}
.freeSpace {
flex: 0 0 auto;
margin-left: 15px;
color: $gray;
text-align: right;
font-size: $smallFontSize;
}

View file

@ -1,61 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatBytes from 'Utilities/Number/formatBytes';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './RootFolderSelectInputSelectedValue.css';
function RootFolderSelectInputSelectedValue(props) {
const {
value,
freeSpace,
movieFolder,
includeFreeSpace,
isWindows,
...otherProps
} = props;
const slashCharacter = isWindows ? '\\' : '/';
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<div className={styles.pathContainer}>
<div className={styles.path}>
{value}
</div>
{
movieFolder ?
<div className={styles.movieFolder}>
{slashCharacter}
{movieFolder}
</div> :
null
}
</div>
{
freeSpace != null && includeFreeSpace &&
<div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free
</div>
}
</EnhancedSelectInputSelectedValue>
);
}
RootFolderSelectInputSelectedValue.propTypes = {
value: PropTypes.string,
freeSpace: PropTypes.number,
movieFolder: PropTypes.string,
isWindows: PropTypes.bool,
includeFreeSpace: PropTypes.bool.isRequired
};
RootFolderSelectInputSelectedValue.defaultProps = {
includeFreeSpace: true
};
export default RootFolderSelectInputSelectedValue;

View file

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Indexer/MoviePoster';
import styles from './MovieSearchResult.css'; import styles from './MovieSearchResult.css';
function MovieSearchResult(props) { function MovieSearchResult(props) {

View file

@ -15,6 +15,11 @@
padding-left: 20px; padding-left: 20px;
} }
.appTitle {
font-size: 30px;
color: #fff
}
.logoFull, .logoFull,
.logo { .logo {
vertical-align: middle; vertical-align: middle;

View file

@ -46,18 +46,14 @@ class PageHeader extends Component {
render() { render() {
const { const {
onSidebarToggle, onSidebarToggle
isSmallScreen
} = this.props; } = this.props;
return ( return (
<div className={styles.header}> <div className={styles.header}>
<div className={styles.logoContainer}> <div className={styles.logoContainer}>
<Link to={`${window.Prowlarr.urlBase}/`}> <Link to={`${window.Prowlarr.urlBase}/`}>
<img <span className={styles.appTitle}>Prowlarr</span>
className={isSmallScreen ? styles.logo : styles.logoFull}
src={isSmallScreen ? `${window.Prowlarr.urlBase}/Content/Images/logo.png` : `${window.Prowlarr.urlBase}/Content/Images/logo-full.png`}
/>
</Link> </Link>
</div> </div>
@ -101,7 +97,6 @@ class PageHeader extends Component {
PageHeader.propTypes = { PageHeader.propTypes = {
onSidebarToggle: PropTypes.func.isRequired, onSidebarToggle: PropTypes.func.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
bindShortcut: PropTypes.func.isRequired bindShortcut: PropTypes.func.isRequired
}; };

View file

@ -14,7 +14,7 @@ import LoadingPage from './LoadingPage';
import Page from './Page'; import Page from './Page';
function testLocalStorage() { function testLocalStorage() {
const key = 'radarrTest'; const key = 'prowlarrTest';
try { try {
localStorage.setItem(key, key); localStorage.setItem(key, key);

View file

@ -3,7 +3,6 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import QueueStatusConnector from 'Activity/Queue/Status/QueueStatusConnector';
import OverlayScroller from 'Components/Scroller/OverlayScroller'; import OverlayScroller from 'Components/Scroller/OverlayScroller';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
@ -26,35 +25,22 @@ const links = [
alias: '/movies', alias: '/movies',
children: [ children: [
{ {
title: translate('AddNew'), title: translate('Stats'),
to: '/add/new' to: '/indexer/stats'
},
{
title: translate('Import'),
to: '/add/import'
} }
] ]
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.SEARCH,
title: 'Search', title: 'Search',
to: '/activity/queue', to: '/search'
children: [
{
title: translate('Queue'),
to: '/activity/queue',
statusComponent: QueueStatusConnector
}, },
{ {
title: translate('History'), iconName: icons.ACTIVITY,
to: '/activity/history' title: 'History',
}, to: '/history'
{
title: translate('Blacklist'),
to: '/activity/blacklist'
}
]
}, },
{ {

View file

@ -7,8 +7,6 @@ import { setAppValue, setVersion } from 'Store/Actions/appActions';
import { removeItem, update, updateItem } from 'Store/Actions/baseActions'; import { removeItem, update, updateItem } from 'Store/Actions/baseActions';
import { fetchCommands, finishCommand, updateCommand } from 'Store/Actions/commandActions'; import { fetchCommands, finishCommand, updateCommand } from 'Store/Actions/commandActions';
import { fetchMovies } from 'Store/Actions/movieActions'; import { fetchMovies } from 'Store/Actions/movieActions';
import { fetchQueue, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchHealth } from 'Store/Actions/systemActions'; import { fetchHealth } from 'Store/Actions/systemActions';
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions'; import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
import { repopulatePage } from 'Utilities/pagePopulator'; import { repopulatePage } from 'Utilities/pagePopulator';
@ -25,12 +23,10 @@ function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.app.isReconnecting, (state) => state.app.isReconnecting,
(state) => state.app.isDisconnected, (state) => state.app.isDisconnected,
(state) => state.queue.paged.isPopulated, (isReconnecting, isDisconnected) => {
(isReconnecting, isDisconnected, isQueuePopulated) => {
return { return {
isReconnecting, isReconnecting,
isDisconnected, isDisconnected
isQueuePopulated
}; };
} }
); );
@ -46,9 +42,6 @@ const mapDispatchToProps = {
dispatchUpdateItem: updateItem, dispatchUpdateItem: updateItem,
dispatchRemoveItem: removeItem, dispatchRemoveItem: removeItem,
dispatchFetchHealth: fetchHealth, dispatchFetchHealth: fetchHealth,
dispatchFetchQueue: fetchQueue,
dispatchFetchQueueDetails: fetchQueueDetails,
dispatchFetchRootFolders: fetchRootFolders,
dispatchFetchMovies: fetchMovies, dispatchFetchMovies: fetchMovies,
dispatchFetchTags: fetchTags, dispatchFetchTags: fetchTags,
dispatchFetchTagDetails: fetchTagDetails dispatchFetchTagDetails: fetchTagDetails
@ -146,16 +139,6 @@ class SignalRConnector extends Component {
console.error(`signalR: Unable to find handler for ${name}`); console.error(`signalR: Unable to find handler for ${name}`);
} }
handleCalendar = (body) => {
if (body.action === 'updated') {
this.props.dispatchUpdateItem({
section: 'calendar',
updateOnly: true,
...body.resource
});
}
}
handleCommand = (body) => { handleCommand = (body) => {
if (body.action === 'sync') { if (body.action === 'sync') {
this.props.dispatchFetchCommands(); this.props.dispatchFetchCommands();
@ -175,19 +158,6 @@ class SignalRConnector extends Component {
} }
} }
handleMoviefile = (body) => {
const section = 'movieFiles';
if (body.action === 'updated') {
this.props.dispatchUpdateItem({ section, ...body.resource });
// Repopulate the page to handle recently imported file
repopulatePage('movieFileUpdated');
} else if (body.action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: body.resource.id });
}
}
handleHealth = () => { handleHealth = () => {
this.props.dispatchFetchHealth(); this.props.dispatchFetchHealth();
} }
@ -203,20 +173,6 @@ class SignalRConnector extends Component {
} }
} }
handleQueue = () => {
if (this.props.isQueuePopulated) {
this.props.dispatchFetchQueue();
}
}
handleQueueDetails = () => {
this.props.dispatchFetchQueueDetails();
}
handleQueueStatus = (body) => {
this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
}
handleVersion = (body) => { handleVersion = (body) => {
const version = body.version; const version = body.version;
@ -227,10 +183,6 @@ class SignalRConnector extends Component {
this.props.dispatchFetchCommands(); this.props.dispatchFetchCommands();
} }
handleRootfolder = () => {
this.props.dispatchFetchRootFolders();
}
handleTag = (body) => { handleTag = (body) => {
if (body.action === 'sync') { if (body.action === 'sync') {
this.props.dispatchFetchTags(); this.props.dispatchFetchTags();
@ -312,7 +264,6 @@ class SignalRConnector extends Component {
SignalRConnector.propTypes = { SignalRConnector.propTypes = {
isReconnecting: PropTypes.bool.isRequired, isReconnecting: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired, isDisconnected: PropTypes.bool.isRequired,
isQueuePopulated: PropTypes.bool.isRequired,
dispatchFetchCommands: PropTypes.func.isRequired, dispatchFetchCommands: PropTypes.func.isRequired,
dispatchUpdateCommand: PropTypes.func.isRequired, dispatchUpdateCommand: PropTypes.func.isRequired,
dispatchFinishCommand: PropTypes.func.isRequired, dispatchFinishCommand: PropTypes.func.isRequired,
@ -322,9 +273,6 @@ SignalRConnector.propTypes = {
dispatchUpdateItem: PropTypes.func.isRequired, dispatchUpdateItem: PropTypes.func.isRequired,
dispatchRemoveItem: PropTypes.func.isRequired, dispatchRemoveItem: PropTypes.func.isRequired,
dispatchFetchHealth: PropTypes.func.isRequired, dispatchFetchHealth: PropTypes.func.isRequired,
dispatchFetchQueue: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchMovies: PropTypes.func.isRequired, dispatchFetchMovies: PropTypes.func.isRequired,
dispatchFetchTags: PropTypes.func.isRequired, dispatchFetchTags: PropTypes.func.isRequired,
dispatchFetchTagDetails: PropTypes.func.isRequired dispatchFetchTagDetails: PropTypes.func.isRequired

View file

@ -4,8 +4,5 @@ export const DATE = 'date';
export const DEFAULT = 'default'; export const DEFAULT = 'default';
export const INDEXER = 'indexer'; export const INDEXER = 'indexer';
export const PROTOCOL = 'protocol'; export const PROTOCOL = 'protocol';
export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile';
export const MOVIE_STATUS = 'movieStatus'; export const MOVIE_STATUS = 'movieStatus';
export const TAG = 'tag'; export const TAG = 'tag';
export const IMPORTLIST = 'importList';

View file

@ -8,8 +8,6 @@ export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
export const PATH = 'path'; export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect'; export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
export const SELECT = 'select'; export const SELECT = 'select';
export const TAG = 'tag'; export const TAG = 'tag';
@ -29,8 +27,6 @@ export const all = [
OAUTH, OAUTH,
PASSWORD, PASSWORD,
PATH, PATH,
QUALITY_PROFILE_SELECT,
ROOT_FOLDER_SELECT,
INDEXER_FLAGS_SELECT, INDEXER_FLAGS_SELECT,
SELECT, SELECT,
TAG, TAG,

View file

@ -5,10 +5,10 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Indexer/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Indexer/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Indexer/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Indexer/MovieTitleLink';
import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css'; import styles from './HistoryRow.css';

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