1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-15 15:54:47 -04:00

Compare commits

..

3 Commits

Author SHA1 Message Date
ta264
38c5989695 Only query 5 movies ahead 2020-05-24 22:47:51 +01:00
ta264
9ca03ba450 Revert "Revert "Revert "Revert "Use async api calls for Refresh and NetImportSync""""
This reverts commit 0479bb9cc3.
2020-05-24 22:47:23 +01:00
ta264
d283967523 Revert "Revert "Async HttpClient and list lookup""
This reverts commit 1d6a7a1843.
2020-05-24 22:47:20 +01:00
2542 changed files with 47984 additions and 115331 deletions

View File

@@ -2,12 +2,6 @@
# editorconfig.org # editorconfig.org
root = true root = true
# NOTE: Requires **VS2019 16.3** or later
# Stylecop.ruleset
# Description: Rules for Radarr
# Code files
[*.cs] [*.cs]
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true
@@ -19,10 +13,10 @@ indent_size = 4
dotnet_sort_system_directives_first = true dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary # Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:warning dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:warning dotnet_style_qualification_for_event = false:refactoring
# Indentation preferences # Indentation preferences
csharp_indent_block_contents = true csharp_indent_block_contents = true
@@ -32,6 +26,10 @@ csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true csharp_indent_switch_labels = true
csharp_indent_labels = flush_left csharp_indent_labels = flush_left
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_naming_style.instance_field_style.capitalization = camel_case dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _ dotnet_naming_style.instance_field_style.required_prefix = _
@@ -40,224 +38,6 @@ csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion csharp_style_var_elsewhere = true:suggestion
# Stylecop Rules
dotnet_diagnostic.SA0001.severity = none
dotnet_diagnostic.SA1005.severity = none
dotnet_diagnostic.SA1025.severity = none
dotnet_diagnostic.SA1101.severity = none
dotnet_diagnostic.SA1116.severity = none
dotnet_diagnostic.SA1118.severity = none
dotnet_diagnostic.SA1122.severity = none
dotnet_diagnostic.SA1201.severity = suggestion
dotnet_diagnostic.SA1202.severity = suggestion
dotnet_diagnostic.SA1204.severity = suggestion
dotnet_diagnostic.SA1300.severity = none
dotnet_diagnostic.SA1303.severity = none
dotnet_diagnostic.SA1304.severity = none
dotnet_diagnostic.SA1306.severity = none
dotnet_diagnostic.SA1309.severity = none
dotnet_diagnostic.SA1310.severity = none
dotnet_diagnostic.SA1401.severity = none
dotnet_diagnostic.SA1402.severity = none
dotnet_diagnostic.SA1404.severity = suggestion
dotnet_diagnostic.SA1405.severity = suggestion
dotnet_diagnostic.SA1406.severity = suggestion
dotnet_diagnostic.SA1410.severity = suggestion
dotnet_diagnostic.SA1411.severity = suggestion
dotnet_diagnostic.SA1413.severity = none
dotnet_diagnostic.SA1516.severity = none
dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1601.severity = none
dotnet_diagnostic.SA1602.severity = none
dotnet_diagnostic.SA1604.severity = none
dotnet_diagnostic.SA1605.severity = none
dotnet_diagnostic.SA1606.severity = none
dotnet_diagnostic.SA1607.severity = none
dotnet_diagnostic.SA1608.severity = none
dotnet_diagnostic.SA1610.severity = none
dotnet_diagnostic.SA1611.severity = none
dotnet_diagnostic.SA1612.severity = none
dotnet_diagnostic.SA1613.severity = none
dotnet_diagnostic.SA1614.severity = none
dotnet_diagnostic.SA1615.severity = none
dotnet_diagnostic.SA1616.severity = none
dotnet_diagnostic.SA1617.severity = none
dotnet_diagnostic.SA1618.severity = none
dotnet_diagnostic.SA1619.severity = none
dotnet_diagnostic.SA1620.severity = none
dotnet_diagnostic.SA1621.severity = none
dotnet_diagnostic.SA1622.severity = none
dotnet_diagnostic.SA1623.severity = none
dotnet_diagnostic.SA1624.severity = none
dotnet_diagnostic.SA1625.severity = none
dotnet_diagnostic.SA1626.severity = none
dotnet_diagnostic.SA1627.severity = none
dotnet_diagnostic.SA1629.severity = none
dotnet_diagnostic.SA1633.severity = none
dotnet_diagnostic.SA1634.severity = none
dotnet_diagnostic.SA1635.severity = none
dotnet_diagnostic.SA1636.severity = none
dotnet_diagnostic.SA1637.severity = none
dotnet_diagnostic.SA1638.severity = none
dotnet_diagnostic.SA1640.severity = none
dotnet_diagnostic.SA1641.severity = none
dotnet_diagnostic.SA1642.severity = none
dotnet_diagnostic.SA1643.severity = none
dotnet_diagnostic.SA1648.severity = none
dotnet_diagnostic.SA1649.severity = none
dotnet_diagnostic.SA1651.severity = none
dotnet_diagnostic.SX1309.severity = warning
# Microsoft Analyzers that fail and need to be sorted thru
dotnet_diagnostic.ASP0000.severity = suggestion
dotnet_diagnostic.CA1000.severity = suggestion
dotnet_diagnostic.CA1001.severity = suggestion
dotnet_diagnostic.CA1002.severity = suggestion
dotnet_diagnostic.CA1003.severity = suggestion
dotnet_diagnostic.CA1008.severity = suggestion
dotnet_diagnostic.CA1010.severity = suggestion
dotnet_diagnostic.CA1012.severity = suggestion
dotnet_diagnostic.CA1014.severity = suggestion
dotnet_diagnostic.CA1016.severity = suggestion
dotnet_diagnostic.CA1017.severity = suggestion
dotnet_diagnostic.CA1018.severity = suggestion
dotnet_diagnostic.CA1019.severity = suggestion
dotnet_diagnostic.CA1021.severity = suggestion
dotnet_diagnostic.CA1024.severity = suggestion
dotnet_diagnostic.CA1027.severity = suggestion
dotnet_diagnostic.CA1028.severity = suggestion
dotnet_diagnostic.CA1030.severity = suggestion
dotnet_diagnostic.CA1031.severity = suggestion
dotnet_diagnostic.CA1032.severity = suggestion
dotnet_diagnostic.CA1033.severity = suggestion
dotnet_diagnostic.CA1034.severity = suggestion
dotnet_diagnostic.CA1036.severity = suggestion
dotnet_diagnostic.CA1040.severity = suggestion
dotnet_diagnostic.CA1041.severity = suggestion
dotnet_diagnostic.CA1043.severity = suggestion
dotnet_diagnostic.CA1044.severity = suggestion
dotnet_diagnostic.CA1050.severity = suggestion
dotnet_diagnostic.CA1051.severity = suggestion
dotnet_diagnostic.CA1052.severity = suggestion
dotnet_diagnostic.CA1054.severity = suggestion
dotnet_diagnostic.CA1055.severity = suggestion
dotnet_diagnostic.CA1056.severity = suggestion
dotnet_diagnostic.CA1058.severity = suggestion
dotnet_diagnostic.CA1060.severity = suggestion
dotnet_diagnostic.CA1061.severity = suggestion
dotnet_diagnostic.CA1062.severity = suggestion
dotnet_diagnostic.CA1063.severity = suggestion
dotnet_diagnostic.CA1064.severity = suggestion
dotnet_diagnostic.CA1065.severity = suggestion
dotnet_diagnostic.CA1066.severity = suggestion
dotnet_diagnostic.CA1067.severity = suggestion
dotnet_diagnostic.CA1068.severity = suggestion
dotnet_diagnostic.CA1069.severity = suggestion
dotnet_diagnostic.CA1200.severity = suggestion
dotnet_diagnostic.CA1303.severity = suggestion
dotnet_diagnostic.CA1304.severity = suggestion
dotnet_diagnostic.CA1305.severity = suggestion
dotnet_diagnostic.CA1307.severity = suggestion
dotnet_diagnostic.CA1308.severity = suggestion
dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion
dotnet_diagnostic.CA1708.severity = suggestion
dotnet_diagnostic.CA1710.severity = suggestion
dotnet_diagnostic.CA1711.severity = suggestion
dotnet_diagnostic.CA1712.severity = suggestion
dotnet_diagnostic.CA1714.severity = suggestion
dotnet_diagnostic.CA1715.severity = suggestion
dotnet_diagnostic.CA1716.severity = suggestion
dotnet_diagnostic.CA1717.severity = suggestion
dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.severity = suggestion
dotnet_diagnostic.CA1801.severity = suggestion
dotnet_diagnostic.CA1802.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion
dotnet_diagnostic.CA1813.severity = suggestion
dotnet_diagnostic.CA1814.severity = suggestion
dotnet_diagnostic.CA1815.severity = suggestion
dotnet_diagnostic.CA1816.severity = suggestion
dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.severity = suggestion
dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.severity = suggestion
dotnet_diagnostic.CA2009.severity = suggestion
dotnet_diagnostic.CA2010.severity = suggestion
dotnet_diagnostic.CA2011.severity = suggestion
dotnet_diagnostic.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion
dotnet_diagnostic.CA2101.severity = suggestion
dotnet_diagnostic.CA2119.severity = suggestion
dotnet_diagnostic.CA2153.severity = suggestion
dotnet_diagnostic.CA2200.severity = suggestion
dotnet_diagnostic.CA2201.severity = suggestion
dotnet_diagnostic.CA2207.severity = suggestion
dotnet_diagnostic.CA2208.severity = suggestion
dotnet_diagnostic.CA2211.severity = suggestion
dotnet_diagnostic.CA2213.severity = suggestion
dotnet_diagnostic.CA2214.severity = suggestion
dotnet_diagnostic.CA2215.severity = suggestion
dotnet_diagnostic.CA2216.severity = suggestion
dotnet_diagnostic.CA2219.severity = suggestion
dotnet_diagnostic.CA2225.severity = suggestion
dotnet_diagnostic.CA2226.severity = suggestion
dotnet_diagnostic.CA2227.severity = suggestion
dotnet_diagnostic.CA2229.severity = suggestion
dotnet_diagnostic.CA2231.severity = suggestion
dotnet_diagnostic.CA2234.severity = suggestion
dotnet_diagnostic.CA2235.severity = suggestion
dotnet_diagnostic.CA2237.severity = suggestion
dotnet_diagnostic.CA2241.severity = suggestion
dotnet_diagnostic.CA2242.severity = suggestion
dotnet_diagnostic.CA2243.severity = suggestion
dotnet_diagnostic.CA2244.severity = suggestion
dotnet_diagnostic.CA2245.severity = suggestion
dotnet_diagnostic.CA2246.severity = suggestion
dotnet_diagnostic.CA3061.severity = suggestion
dotnet_diagnostic.CA3075.severity = suggestion
dotnet_diagnostic.CA3076.severity = suggestion
dotnet_diagnostic.CA3077.severity = suggestion
dotnet_diagnostic.CA3147.severity = suggestion
dotnet_diagnostic.CA5350.severity = suggestion
dotnet_diagnostic.CA5351.severity = suggestion
dotnet_diagnostic.CA5359.severity = suggestion
dotnet_diagnostic.CA5360.severity = suggestion
dotnet_diagnostic.CA5363.severity = suggestion
dotnet_diagnostic.CA5364.severity = suggestion
dotnet_diagnostic.CA5365.severity = suggestion
dotnet_diagnostic.CA5366.severity = suggestion
dotnet_diagnostic.CA5368.severity = suggestion
dotnet_diagnostic.CA5369.severity = suggestion
dotnet_diagnostic.CA5370.severity = suggestion
dotnet_diagnostic.CA5371.severity = suggestion
dotnet_diagnostic.CA5372.severity = suggestion
dotnet_diagnostic.CA5373.severity = suggestion
dotnet_diagnostic.CA5374.severity = suggestion
dotnet_diagnostic.CA5379.severity = suggestion
dotnet_diagnostic.CA5384.severity = suggestion
dotnet_diagnostic.CA5385.severity = suggestion
dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion
[*.{js,html,js,hbs,less,css}] [*.{js,html,js,hbs,less,css}]
charset = utf-8 charset = utf-8
trim_trailing_whitespace = true trim_trailing_whitespace = true

View File

@@ -1,7 +1,6 @@
{ {
"paths": [ "paths": [
"frontend/src/**/*.js", "frontend/src/**/*.js"
"src/NzbDrone.Core/Localization/Core/*.json"
], ],
"ignored": [ "ignored": [
"**/node_modules/**/*" "**/node_modules/**/*"

305
.gitchangelog.rc Normal file
View File

@@ -0,0 +1,305 @@
# -*- 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/Radarr/Radarr/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/Radarr/Radarr/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..."]

311
.gitchangelog.rc.release Normal file
View File

@@ -0,0 +1,311 @@
# -*- 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/Radarr/Radarr/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+"..."]

22
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@@ -0,0 +1,22 @@
**Description:**
<!-- Check first that your problem is not listed in our wiki section:
* https://github.com/Radarr/Radarr/wiki/Common-Problems
* https://github.com/Radarr/Radarr/wiki/FAQ
**Just because you receive an exception in your logs, doesn't mean it's a bug and should be reported here. Often it's something else, such as a permission error. If you are unsure ask on the Discord or Subreddit first.**
Visit our [Discord server](https://discord.gg/NWYch8M) or [Subreddit](https://reddit.com/r/radarr) for support or longer discussions. Support questions posed on here will be closed immediately.
Provide a description of the feature request or bug here, the more details the better.
Please also include the following if you are reporting a bug. If you do not include it, the issue will probably be closed as we cannot help you. -->
**Radarr Version:**
**Mono Version:**
**Debug Logs:**
# Do not remove anything from your logs and post the full logs! If not everything fits in here use a service like https://pastebin.com to upload them.
<!-- Please use the search bar (make sure to include closed issues as well) and make sure you are not submitting an already submitted issue. -->

34
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,34 @@
---
name: Bug report
about: Support requests will be closed immediately, if you are unsure go to our Discord
or Subreddit first. Exceptions do not mean you found a bug!
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Platform Information (please complete the following information):**
- OS: [e.g. Windows]
- Mono Version: [e.g. Mono 5.8] (Only needed under Linux and Mac, found under System -> Status)
- Browser and Version [e.g. chrome, safari] (Only needed for UI issues)
- Radarr Version [e.g. 3.0.0.2956]
**Debug Logs**
Turn on debug logs under Settings -> General and wait for the bug to occur again. **Upload the full log file here (or another site and link it). Issues will be closed, if they do not include this!**

View File

@@ -1,73 +0,0 @@
name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Radarr**: Radarr 3.0.1.4259
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
value: |
- OS:
- Radarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Master
- Develop
- Nightly
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Trace Logs?
description: |
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Support via Discord - name: Support via Discord
url: https://radarr.video/discord url: https://discord.gg/AD3UP37
about: Chat with users and devs on support and setup related topics. about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit - name: Support via Reddit
url: https://reddit.com/r/radarr url: https://reddit.com/r/radarr

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: feature request
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,38 +0,0 @@
name: Feature Request
description: 'Suggest an idea for Radarr'
labels: ['Type: Feature Request', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true

View File

@@ -1,16 +1,12 @@
#### Database Migration #### Database Migration
YES - XXXX | NO YES | NO
#### Description #### Description
A few sentences describing the overall goals of the pull request's commits.
#### Screenshot (if UI related)
#### Todos #### Todos
- [ ] Tests - [ ] Tests
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
- [ ] [Wiki Updates](https://wiki.servarr.com)
#### Issues Fixed or Closed by this PR #### Issues Fixed or Closed by this PR
* Fixes #XXXX * #

2
.github/config.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
todo:
keyword: "TODO"

1
.github/reaction.yml vendored Normal file
View File

@@ -0,0 +1 @@

10
.github/stale.yml vendored
View File

@@ -4,12 +4,10 @@ daysUntilStale: 60
daysUntilClose: 7 daysUntilClose: 7
# Issues with these labels will never be considered stale # Issues with these labels will never be considered stale
exemptLabels: exemptLabels:
- feature request #legacy - feature request
- 'Type: Feature Request' - parser
- 'Status: Confirmed' - confirmed
- sonarr-pull - aphrodite
- lidarr-pull
- readarr-pull
# Label to use when marking an issue as stale # Label to use when marking an issue as stale
staleLabel: stale staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable # Comment to post when marking an issue as stale. Set to `false` to disable

13
.github/support.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# Configuration for support-requests - https://github.com/dessant/support-requests
# Label used to mark issues as support requests
supportLabel: support
# Comment to post on issues marked as support requests. Add a link
# to a support page, or set to `false` to disable
supportComment: >
We use the issue tracker exclusively for bug reports and feature requests.
However, this issue appears to be a support request. Please hop over onto our [Discord](https://discord.gg/ZDmT7qb) or [Subreddit](https://reddit.com/r/radarr)
# Whether to close issues marked as support requests
close: true
# Whether to lock issues marked as support requests
lock: false

View File

@@ -1,41 +0,0 @@
name: Sync issue to Azure DevOps work item
on:
issues:
types:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
concurrency: azuresync-${{ github.event.issue.number }}
jobs:
alert:
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "Bug"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "User Story"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100

View File

@@ -1,21 +0,0 @@
name: 'Lock threads'
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '90'
issue-exclude-created-before: ''
issue-exclude-labels: ''
issue-lock-labels: ''
issue-lock-comment: ''
issue-lock-reason: 'resolved'
process-only: ''

View File

@@ -1,21 +0,0 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v2
with:
github-token: ${{ github.token }}
support-label: 'Type: Support'
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://radarr.video/discord)
or [Subreddit](https://reddit.com/r/radarr)
close-issue: true
lock-issue: false

4
.gitignore vendored
View File

@@ -187,10 +187,6 @@ packages.config.md5sum
**/.idea/**/*.iml **/.idea/**/*.iml
**/.idea/**/contentModel.xml **/.idea/**/contentModel.xml
**/.idea/**/modules.xml **/.idea/**/modules.xml
# ignore node_modules symlink # ignore node_modules symlink
node_modules node_modules
node_modules.nosync node_modules.nosync
# API doc generation
.config/

1096
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,33 @@
# How to Contribute # How to Contribute #
We're always looking for people to help make Radarr even better, there are a number of ways to contribute. We're always looking for people to help make Radarr even better, there are a number of ways to contribute.
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/radarr/contributing). ## Documentation ##
Setup guides, FAQ, the more information we have on the wiki the better.
## Documentation ## Development ##
Setup guides, [FAQ](https://wiki.servarr.com/radarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/radarr) the better. See the readme for information on setting up your development environment.
## Development ### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Radarr/Radarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
- Rebase from Radarr's develop branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the forums or on IRC if you have any questions
- Add tests (unit/integration)
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2012 and WebStorm (to my knowledge)
See the [Wiki Page](https://wiki.servarr.com/radarr/contributing) ### Pull Requesting ###
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
- new-feature (Good)
- fix-bug (Good)
- patch (Bad)
- develop (Bad)
If you have any questions about any of this, please let us know.

18
DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,18 @@
# New UI Development
This document should provide an overview of current UI development, progress and blockers.
## Current Focus
Our current focus is creating a foundation for the UI, so that everything can be built upon it.
We are trialing the Sonarr V3 UI as our foundation. So far it has been working great and we already have a working build running.
## Performance Issues
You can download a database with 40k movies here: https://radarr.video/dev/radarr.db (Version where the next refresh movie scan is in a year. The refresh movie scan will lag the UI and other stuff. https://radarr.video/dev/radarr_no_scan.db). Just place it in your AppData Directory while Radarr is not running and make sure it's named radarr.db (https://github.com/Radarr/Radarr/wiki/AppData-Directory).
You will have to message me (@galli-leo) via Discord or Reddit for the username and password (just as a precaution).
## Tasks
The actual tasks that are not related to the foundation of the new UI are all listed here https://github.com/Radarr/Radarr/projects/4. They are sorted according to different priorities. Some issues are also issues that shouldn't need anything extra, just a correct implementation in the new UI.

156
README.md
View File

@@ -1,21 +1,76 @@
# Radarr # Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop) **New UI Development:** For an overview of the new UI development see [DEVELOPMENT.md](https://github.com/Radarr/Radarr/blob/aphrodite/DEVELOPMENT.md).
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)
[![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors)
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available. Radarr is an __independent__ fork of [Sonarr](https://github.com/Sonarr/Sonarr) reworked for automatically downloading movies via Usenet and BitTorrent.
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
## Major Features Include The project was inspired by other Usenet/BitTorrent movie downloaders such as CouchPotato.
See the [Roadmap blogpost](https://blog.radarr.video/development/update/2018/11/11/roadmap-update.html) for an overview of planned features.
## Getting Started
[![Installation](https://img.shields.io/badge/wiki-installation-brightgreen.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/Installation)
[![Docker](https://img.shields.io/badge/wiki-docker-1488C6.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/Docker)
[![Setup Guide](https://img.shields.io/badge/wiki-setup_guide-orange.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/Setup-Guide)
[![FAQ](https://img.shields.io/badge/wiki-FAQ-BF55EC.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki/FAQ)
* [Install Radarr for your desired OS](https://github.com/Radarr/Radarr/wiki/Installation) *or* use [Docker](https://github.com/Radarr/Radarr/wiki/Docker)
* *For Linux users*, run `radarr` and *optionally* have [Radarr start automatically](https://github.com/Radarr/Radarr/wiki/Autostart-on-Linux)
* Connect to the UI through <http://localhost:7878> or <http://your-ip:7878> in your web browser
* See the [Setup Guide](https://github.com/Radarr/Radarr/wiki/Setup-Guide) for further configuration
## Downloads
| Release Type | Branch: develop (stable) | Branch: nightly (semi-unstable) | Branch: aphrodite (very-unstable) |
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Binary Releases | [![GitHub Releases](https://img.shields.io/badge/downloads-releases-brightgreen.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/releases) | [![AppVeyor Builds](https://img.shields.io/badge/downloads-nightly-green.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/radarr-usby1/branch/develop/artifacts) | |
| Docker | [![Docker release](https://img.shields.io/badge/linuxserver-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) | [![Docker nightly](https://img.shields.io/badge/linuxserver-radarr:nightly-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) | [![Docker aphrodite](https://img.shields.io/badge/linuxserver-radarr:preview-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr) |
| Docker | [![Docker release](https://img.shields.io/badge/hotio-radarr:latest-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) | [![Docker nightly](https://img.shields.io/badge/hotio-radarr:unstable-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) | [![Docker aphrodite](https://img.shields.io/badge/hotio-radarr:aphrodite-blue.svg?colorB=1488C6&maxAge=60&style=flat-square)](https://hub.docker.com/r/hotio/radarr) |
## Support
[![OpenCollective](https://opencollective.com/radarr/tiers/backer/badge.svg)](#backers)
[![OpenCollective](https://opencollective.com/radarr/tiers/flexible-sponsor/badge.svg)](#flexible-sponsors)
[![OpenCollective](https://opencollective.com/radarr/tiers/sponsor/badge.svg)](#sponsors)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60&style=flat-square)](https://discord.gg/AD3UP37)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60&style=flat-square)](https://www.reddit.com/r/radarr)
[![Feathub](https://img.shields.io/badge/feathub-requests-lightgrey.svg?maxAge=60&style=flat-square)](http://feathub.com/Radarr/Radarr)
[![GitHub](https://img.shields.io/badge/github-issues-red.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/issues)
[![GitHub Wiki](https://img.shields.io/badge/github-wiki-181717.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/wiki)
## Status
[![GitHub issues](https://img.shields.io/github/issues/radarr/radarr.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/issues)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/radarr/radarr.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/pulls)
[![GNU GPL v3](https://img.shields.io/badge/license-GNU%20GPL%20v3-blue.svg?maxAge=60&style=flat-square)](http://www.gnu.org/licenses/gpl.html)
[![Copyright 2010-2017](https://img.shields.io/badge/copyright-2017-blue.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr)
[![Github Releases](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg?maxAge=60&style=flat-square)](https://github.com/Radarr/Radarr/releases/)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg?maxAge=60&style=flat-square)](https://hub.docker.com/r/linuxserver/radarr/)
[![Changelog](https://img.shields.io/github/commit-activity/w/radarr/radarr.svg?style=flat-square)](/CHANGELOG.md#unreleased)
| Service | Master | Develop |
|----------|:---------------------------:|:----------------------------:|
| AppVeyor | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr/master.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr) | [![AppVeyor](https://img.shields.io/appveyor/ci/galli-leo/Radarr-usby1/develop.svg?maxAge=60&style=flat-square)](https://ci.appveyor.com/project/galli-leo/Radarr-usby1) |
| Travis | [![Travis](https://img.shields.io/travis/Radarr/Radarr/master.svg?maxAge=60&style=flat-square)](https://travis-ci.org/Radarr/Radarr) | [![Travis](https://img.shields.io/travis/Radarr/Radarr/develop.svg?maxAge=60&style=flat-square)](https://travis-ci.org/Radarr/Radarr) |
### [Site and API Status](https://status.radarr.video)
| API | Updates | Sites |
|-------|:----:|:----:|
| [![API V2 (develop)](http://status.radarr.video/component/1/shield?style=flat-square)](https://api.radarr.video/v2/) | [![Update Server](http://status.radarr.video/component/4/shield?style=flat-square)](https://radarr.aeonlucid.com) | [![Radarr Mappings](http://status.radarr.video/component/6/shield?style=flat-square)](https://mappings.radarr.video/)
| [![API Staging (nightly)](http://status.radarr.video/component/2/shield?style=flat-square)](https://staging.api.radarr.video/) | [![Github Updates](http://status.radarr.video/component/5/shield?style=flat-square)](https://api.github.com/v3/) | [![Main Site](http://status.radarr.video/component/7/shield?style=flat-square)](https://radarr.video/)
Radarr is currently undergoing rapid development and pull requests are actively added into the repository.
## Features
### Current Features
* Adding new movies with lots of information, such as trailers, ratings, etc. * Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc. * Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray* * Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails * Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically * Manual search so you can pick any release or to see why a release was not downloaded automatically
* Full integration with SABnzbd and NZBGet * Full integration with SABnzbd and NZBGet
@@ -23,60 +78,83 @@ Note that only one type of a given movie is supported. If you want both an 4k ve
* Automatically importing downloaded movies * Automatically importing downloaded movies
* Recognizing Special Editions, Director's Cut, etc. * Recognizing Special Editions, Director's Cut, etc.
* Identifying releases with hardcoded subs * Identifying releases with hardcoded subs
* Identifying releases with AKA movie names * All indexers supported by Sonarr also supported
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated * New PassThePopcorn Indexer
* Full integration with Kodi and Plex (notifications, library updates) * QBittorrent, Deluge, rTorrent, Transmission and uTorrent download client (Other clients are coming)
* New TorrentPotato Indexer
* Torznab Indexer now supports Movies (Works well with [Jackett](https://github.com/Jackett/Jackett))
* Scanning PreDB to know when a new release is available
* Importing movies from various online sources, such as IMDb Watchlists (A complete list can be found [here](https://github.com/Radarr/Radarr/issues/114))
* Full integration with Kodi, Plex (notification, library update)
* And a beautiful UI
* Importing Metadata such as trailers or subtitles * Importing Metadata such as trailers or subtitles
* Adding metadata such as posters and information for Kodi and others to use * Adding metadata such as posters and information for Kodi and others to use
* Advanced customization for profiles, such that Radarr will always download the copy you want * Advanced customization for profiles, such that Radarr will always download the copy you want
* A beautiful UI
## Support ### Planned Features
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr) See the [Roadmap blogpost](https://blog.radarr.video/development/update/2018/11/11/roadmap-update.html) for an overview of planned features.
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
Note: GitHub Issues are for Bugs and Feature Requests Only #### [Feature Requests](http://feathub.com/Radarr/Radarr)
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Radarr/Radarr/issues) ## Configuring the Development Environment
## Contributors & Developers ### Requirements
[API Documentation](https://radarr.video/docs/api/) * [Visual Studio Community 2019](https://www.visualstudio.com/vs/community/) or [Rider](http://www.jetbrains.com/rider/)
* [Git](https://git-scm.com/downloads)
* [Node.js](https://nodejs.org/en/download/)
* [Yarn](https://yarnpkg.com/)
This project exists thanks to all the people who contribute. ### Setup
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/radarr/contributing)
[![Contributors List](https://opencollective.com/Radarr/contributors.svg?width=890&button=false)](https://github.com/Radarr/Radarr/graphs/contributors) * Make sure all the required software mentioned above are installed
* Clone the repository into your development machine ([*info*](https://help.github.com/desktop/guides/contributing/working-with-your-remote-repository-on-github-or-github-enterprise))
* Grab the submodules `git submodule init && git submodule update`
* Install the required Node Packages `yarn install`
* Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
## Backers > **Notice**
> Gulp must be running at all times while you are working with Radarr client source files.
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Radarr#backer) ### Build
[![Backers List](https://opencollective.com/Radarr/backers.svg?width=890)](https://opencollective.com/Radarr#backer) * To build run `sh build.sh`
## Sponsors **Note:** Windows users must have bash available to do this. If you installed git, you should have a git bash utility that works.
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/Radarr#sponsor) ### Development
[![Sponsors List](https://opencollective.com/Radarr/sponsors.svg?width=890)](https://opencollective.com/Radarr#sponsor) * Open `Radarr.sln` in Visual Studio 2017 or run the build.sh script, if Mono is installed. Alternatively you can use Jetbrains Rider, since it works on all Platforms.
* Make sure `NzbDrone.Console` is set as the startup project
* Run `build.sh` before running
## Mega Sponsors ## Supporters
[![Mega Sponsors List](https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/Radarr#mega-sponsor) This project would not be possible without the support by these amazing folks. [**Become a sponsor or backer**](https://opencollective.com/radarr) to help us out!
## JetBrains ### Sponsors
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools. [![Sponsors](https://opencollective.com/radarr/tiers/sponsor.svg)](https://opencollective.com/radarr/order/3851)
### Flexible Sponsors
[![Flexible Sponsors](https://opencollective.com/radarr/tiers/flexible-sponsor.svg?avatarHeight=54)](https://opencollective.com/radarr/order/3856)
### Backers
[![Backers](https://opencollective.com/radarr/tiers/backer.svg?avatarHeight=48)](https://opencollective.com/radarr/order/3850)
### JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/) * [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/) * [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/) * [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/) * [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### License ## License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022 * Copyright 2010-2019

3
appveyor.yml Normal file
View File

@@ -0,0 +1,3 @@
skip_commits:
files:
- '**/**'

File diff suppressed because it is too large Load Diff

171
build.sh
View File

@@ -1,4 +1,4 @@
#! /usr/bin/env bash #! /bin/bash
set -e set -e
outputFolder='_output' outputFolder='_output'
@@ -25,25 +25,6 @@ UpdateVersionNumber()
fi fi
} }
EnableExtraPlatformsInSDK()
{
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
fi
}
LintUI() LintUI()
{ {
ProgressStart 'ESLint' ProgressStart 'ESLint'
@@ -90,15 +71,15 @@ Build()
YarnInstall() YarnInstall()
{ {
ProgressStart 'yarn install' ProgressStart 'yarn install'
yarn install --frozen-lockfile --network-timeout 120000 yarn install --frozen-lockfile
ProgressEnd 'yarn install' ProgressEnd 'yarn install'
} }
RunWebpack() RunGulp()
{ {
ProgressStart 'Running webpack' ProgressStart 'Running gulp'
yarn run build --env production yarn run build --production
ProgressEnd 'Running webpack' ProgressEnd 'Running gulp'
} }
PackageFiles() PackageFiles()
@@ -137,7 +118,7 @@ PackageLinux()
echo "Adding Radarr.Mono to UpdatePackage" echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then if [ "$framework" = "netcoreapp3.1" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi fi
@@ -148,13 +129,17 @@ PackageLinux()
PackageMacOS() PackageMacOS()
{ {
local framework="$1" local framework="$1"
local runtime="$2"
ProgressStart "Creating MacOS Package for $framework $runtime" ProgressStart "Creating MacOS Package for $framework"
local folder=$artifactsFolder/$runtime/$framework/Radarr local folder=$artifactsFolder/macos/$framework/Radarr
PackageFiles "$folder" "$framework" "$runtime" PackageFiles "$folder" "$framework" "osx-x64"
if [ "$framework" = "net462" ]; then
echo "Adding Startup script"
cp macOS/Radarr $folder
fi
echo "Removing Service helpers" echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.* rm -f $folder/ServiceUninstall.*
@@ -165,7 +150,7 @@ PackageMacOS()
echo "Adding Radarr.Mono to UpdatePackage" echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then if [ "$framework" = "netcoreapp3.1" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi fi
@@ -176,11 +161,10 @@ PackageMacOS()
PackageMacOSApp() PackageMacOSApp()
{ {
local framework="$1" local framework="$1"
local runtime="$2"
ProgressStart "Creating macOS App Package for $framework $runtime" ProgressStart "Creating macOS App Package for $framework"
local folder="$artifactsFolder/$runtime-app/$framework" local folder=$artifactsFolder/macos-app/$framework
rm -rf $folder rm -rf $folder
mkdir -p $folder mkdir -p $folder
@@ -188,7 +172,7 @@ PackageMacOSApp()
mkdir -p $folder/Radarr.app/Contents/MacOS mkdir -p $folder/Radarr.app/Contents/MacOS
echo "Copying Binaries" echo "Copying Binaries"
cp -r $artifactsFolder/$runtime/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS cp -r $artifactsFolder/macos/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
echo "Removing Update Folder" echo "Removing Update Folder"
rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update
@@ -199,14 +183,12 @@ PackageMacOSApp()
PackageWindows() PackageWindows()
{ {
local framework="$1" local framework="$1"
local runtime="$2"
ProgressStart "Creating Windows Package for $framework" ProgressStart "Creating Windows Package for $framework"
local folder=$artifactsFolder/$runtime/$framework/Radarr local folder=$artifactsFolder/windows/$framework/Radarr
PackageFiles "$folder" "$framework" "$runtime" PackageFiles "$folder" "$framework" "win-x64"
cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder
echo "Removing Radarr.Mono" echo "Removing Radarr.Mono"
rm -f $folder/Radarr.Mono.* rm -f $folder/Radarr.Mono.*
@@ -228,45 +210,19 @@ Package()
IFS='-' read -ra SPLIT <<< "$runtime" IFS='-' read -ra SPLIT <<< "$runtime"
case "${SPLIT[0]}" in case "${SPLIT[0]}" in
linux|freebsd*) linux)
PackageLinux "$framework" "$runtime" PackageLinux "$framework" "$runtime"
;; ;;
win) win)
PackageWindows "$framework" "$runtime" PackageWindows "$framework"
;; ;;
osx) osx)
PackageMacOS "$framework" "$runtime" PackageMacOS "$framework"
PackageMacOSApp "$framework" "$runtime" PackageMacOSApp "$framework"
;; ;;
esac esac
} }
BuildInstaller()
{
local framework="$1"
local runtime="$2"
./_inno/ISCC.exe setup/radarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
}
InstallInno()
{
ProgressStart "Installing portable Inno Setup"
rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe
ProgressEnd "Installed portable Inno Setup"
}
RemoveInno()
{
rm -rf _inno
}
PackageTests() PackageTests()
{ {
local framework="$1" local framework="$1"
@@ -276,6 +232,14 @@ PackageTests()
rm -f $testPackageFolder/$framework/$runtime/*.log.config rm -f $testPackageFolder/$framework/$runtime/*.log.config
# geckodriver.exe isn't copied by dotnet publish
if [ "$runtime" = "win-x64" ];
then
curl -Lso gecko.zip "https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-win64.zip"
unzip -o gecko.zip
cp geckodriver.exe "$testPackageFolder/$framework/win-x64/publish"
fi
ProgressEnd 'Creating Test Package' ProgressEnd 'Creating Test Package'
} }
@@ -298,10 +262,7 @@ if [ $# -eq 0 ]; then
BACKEND=YES BACKEND=YES
FRONTEND=YES FRONTEND=YES
PACKAGES=YES PACKAGES=YES
INSTALLER=NO
LINT=YES LINT=YES
ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -313,14 +274,6 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd|--enable-extra-platforms)
ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument
;;
-r|--runtime) -r|--runtime)
RID="$2" RID="$2"
shift # past argument shift # past argument
@@ -339,10 +292,6 @@ case $key in
PACKAGES=YES PACKAGES=YES
shift # past argument shift # past argument
;; ;;
--installer)
INSTALLER=YES
shift # past argument
;;
--lint) --lint)
LINT=YES LINT=YES
shift # past argument shift # past argument
@@ -362,31 +311,16 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
EnableExtraPlatforms
fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then then
PackageTests "net6.0" "win-x64" PackageTests "netcoreapp3.1" "win-x64"
PackageTests "net6.0" "win-x86" PackageTests "netcoreapp3.1" "linux-x64"
PackageTests "net6.0" "linux-x64" PackageTests "netcoreapp3.1" "osx-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net462" "linux-x64"
PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
fi fi
@@ -395,7 +329,7 @@ fi
if [ "$FRONTEND" = "YES" ]; if [ "$FRONTEND" = "YES" ];
then then
YarnInstall YarnInstall
RunWebpack RunGulp
fi fi
if [ "$LINT" = "YES" ]; if [ "$LINT" = "YES" ];
@@ -414,30 +348,13 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then then
Package "net6.0" "win-x64" Package "netcoreapp3.1" "win-x64"
Package "net6.0" "win-x86" Package "netcoreapp3.1" "linux-x64"
Package "net6.0" "linux-x64" Package "netcoreapp3.1" "linux-arm64"
Package "net6.0" "linux-musl-x64" Package "netcoreapp3.1" "linux-arm"
Package "net6.0" "linux-arm64" Package "netcoreapp3.1" "osx-x64"
Package "net6.0" "linux-musl-arm64" Package "net462" "linux-x64"
Package "net6.0" "linux-arm"
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"
fi fi
fi fi
if [ "$INSTALLER" = "YES" ];
then
InstallInno
BuildInstaller "net6.0" "win-x64"
BuildInstaller "net6.0" "win-x86"
RemoveInno
fi

14
changelog.tpl Normal file
View File

@@ -0,0 +1,14 @@
# Changelog
{{#versions}}
## {{{label}}}
{{#sections}}
### {{{label}}}
{{#commits}}
- {{{subject}}} [<a href="https://github.com/{{{author}}}">{{{author}}}</a>]
{{/commits}}
{{/sections}}
{{/versions}}

15
changelog_release.tpl Normal file
View File

@@ -0,0 +1,15 @@
**To receive further Pre-Release updates, please change the branch to develop. (Settings -> General (Show Advanced Settings) -> Updates -> Branch)**
{{#versions}}
{{#sections}}
{{{label}}}
{{#commits}}
- {{{subject}}} [{{{author}}}]
{{/commits}}
{{/sections}}
{{/versions}}
**Note**: The OSX version does not automatically launch the browser. You have to go to http://localhost:7878 by yourself in a browser of your choice.

38
docs.sh
View File

@@ -1,38 +0,0 @@
PLATFORM=$1
if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-x64"
elif [ "$PLATFORM" = "Linux" ]; then
WHERE="linux-x64"
elif [ "$PLATFORM" = "Mac" ]; then
WHERE="osx-x64"
else
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
exit 1
fi
outputFolder='_output'
testPackageFolder='_tests'
rm -rf $outputFolder
rm -rf $testPackageFolder
slnFile=src/Radarr.sln
platform=Posix
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
sleep 45
kill %1
exit 0

293
frontend/.eslintrc Normal file
View File

@@ -0,0 +1,293 @@
{
"parser": "babel-eslint",
"env": {
"browser": true,
"commonjs": true,
"node": true,
"es6": true
},
"globals": {
"expect": false,
"chai": false,
"sinon": false
},
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"modules": true,
"impliedStrict": true
}
},
"plugins": [
"filenames",
"react"
],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"filenames/match-exported": ["error"],
# ECMAScript 6
"arrow-body-style": [0],
"arrow-parens": ["error", "always"],
"arrow-spacing": ["error", { "before": true, "after": true }],
"constructor-super": "error",
"generator-star-spacing": "off",
"no-class-assign": "error",
"no-confusing-arrow": "error",
"no-const-assign": "error",
"no-dupe-class-members": "error",
"no-duplicate-imports": "error",
"no-new-symbol": "error",
"no-this-before-super": "error",
"no-useless-escape": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-var": "warn",
"object-shorthand": ["error", "properties"],
"prefer-arrow-callback": "error",
"prefer-const": "warn",
"prefer-reflect": "off",
"prefer-rest-params": "off",
"prefer-spread": "warn",
"prefer-template": "error",
"require-yield": "off",
"template-curly-spacing": ["error", "never"],
"yield-star-spacing": "off",
# Possible Errors
"comma-dangle": "error",
"no-cond-assign": "error",
"no-console": "off",
"no-constant-condition": "warn",
"no-control-regex": "error",
"no-debugger": "off",
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "warn",
"no-empty-character-class": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-extra-parens": ["error", "functions"],
"no-extra-semi": "error",
"no-func-assign": "error",
"no-inner-declarations": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-negated-in-lhs": "error",
"no-obj-calls": "error",
"no-regex-spaces": "error",
"no-sparse-arrays": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "warn",
"no-unsafe-finally": "error",
"use-isnan": "error",
"valid-jsdoc": "off",
"valid-typeof": "error",
# Best Practices
"accessor-pairs": "off",
"array-callback-return": "warn",
"block-scoped-var": "warn",
"consistent-return": "off",
"curly": "error",
"default-case": "error",
"dot-location": ["error", "property"],
"dot-notation": "error",
"eqeqeq": ["error", "smart"],
"guard-for-in": "error",
"no-alert": "warn",
"no-caller": "error",
"no-case-declarations": "error",
"no-div-regex": "error",
"no-else-return": "error",
"no-empty-function": ["error", {"allow": ["arrowFunctions"]}],
"no-empty-pattern": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-fallthrough": "error",
"no-floating-decimal": "error",
"no-implicit-coercion": ["error", {
"boolean": false,
"number": true,
"string": true,
"allow": [/* "!!", "~", "*", "+" */]
}],
"no-implicit-globals": "error",
"no-implied-eval": "error",
"no-invalid-this": "off",
"no-iterator": "error",
"no-labels": "error",
"no-lone-blocks": "error",
"no-loop-func": "error",
"no-magic-numbers": ["off", {"ignoreArrayIndexes": true, "ignore": [0, 1] }],
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-native-reassign": ["error", {"exceptions": ["console"]}],
"no-new": "off",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-param-reassign": "off",
"no-process-env": "off",
"no-proto": "error",
"no-redeclare": "error",
"no-return-assign": "warn",
"no-script-url": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-throw-literal": "error",
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-useless-call": "error",
"no-useless-concat": "error",
"no-void": "error",
"no-warning-comments": "off",
"no-with": "error",
"radix": ["error", "as-needed"],
"vars-on-top": "off",
"wrap-iife": ["error", "inside"],
"yoda": "error",
# Strict Mode
"strict": ["error", "never"],
# Variables
"init-declarations": ["error", "always"],
"no-catch-shadow": "error",
"no-delete-var": "error",
"no-label-var": "error",
"no-restricted-globals": "off",
"no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-undef": "error",
"no-undef-init": "off",
"no-undefined": "off",
"no-unused-vars": ["error", { "args": "none", "ignoreRestSiblings": true }],
"no-use-before-define": "error",
# Node.js and CommonJS
"callback-return": "warn",
"global-require": "error",
"handle-callback-err": "warn",
"no-mixed-requires": "error",
"no-new-require": "error",
"no-path-concat": "error",
"no-process-exit": "error",
# Stylistic Issues
"array-bracket-spacing": ["error", "never"],
"block-spacing": ["error", "always"],
"brace-style": ["error", "1tbs", { "allowSingleLine": false }],
"camelcase": "off",
"comma-spacing": ["error", {"before": false, "after": true}],
"comma-style": ["error", "last"],
"computed-property-spacing": ["error", "never"],
"consistent-this": ["error", "self"],
"eol-last": "error",
"func-names": "off",
"func-style": ["error", "declaration"],
"indent": ["error", 2, {"SwitchCase": 1}],
"key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
"keyword-spacing": ["error", { "before": true, "after": true}],
"lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }],
"max-depth": ["error", {"maximum": 5}],
"max-nested-callbacks": ["error", 4],
"max-statements": "off",
"max-statements-per-line": ["error", { "max": 1 }],
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred", "DragDropContext", "DragLayer", "DragSource", "DropTarget"]}],
"new-parens": "error",
"newline-after-var": "off",
"newline-before-return": "off",
"newline-per-chained-call": "off",
"no-array-constructor": "error",
"no-bitwise": "error",
"no-continue": "error",
"no-inline-comments": "off",
"no-lonely-if": "warn",
"no-mixed-spaces-and-tabs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"no-negated-condition": "warn",
"no-nested-ternary": "error",
"no-new-object": "error",
"no-plusplus": "off",
"no-restricted-syntax": "off",
"no-spaced-func": "error",
"no-ternary": "off",
"no-trailing-spaces": "error",
"no-underscore-dangle": ["error", { "allowAfterThis": true }],
"no-unneeded-ternary": "error",
"no-whitespace-before-property": "error",
"object-curly-spacing": ["error", "always"],
"one-var": ["error", "never"],
"one-var-declaration-per-line": ["error", "always"],
"operator-assignment": ["off", "never"],
"operator-linebreak": ["error", "after"],
"quote-props": ["error", "as-needed"],
"quotes": ["error", "single"],
"require-jsdoc": "off",
"semi": "error",
"semi-spacing": ["error", { "before": false, "after": true }],
"sort-vars": "off",
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", "never"],
"space-in-parens": "off",
"space-infix-ops": "off",
"space-unary-ops": "off",
"spaced-comment": "error",
"wrap-regex": "error",
# React
"react/jsx-boolean-value": [2, "always"],
"react/jsx-uses-vars": 2,
"react/jsx-closing-bracket-location": 2,
"react/jsx-tag-spacing": ["error"],
"react/jsx-curly-spacing": [2, "never"],
"react/jsx-equals-spacing": [2, "never"],
"react/jsx-indent-props": [2, 2],
"react/jsx-indent": [2, 2, { "indentLogicalExpressions": true }],
"react/jsx-key": 2,
"react/jsx-no-bind": [2, { "allowArrowFunctions": true }],
"react/jsx-no-duplicate-props": [2, { "ignoreCase": true }],
"react/jsx-max-props-per-line": [2, { "maximum": 2 }],
"react/jsx-handler-names": [2, { "eventHandlerPrefix": "(on|dispatch)", "eventHandlerPropPrefix": "on" }],
"react/jsx-no-undef": 2,
"react/jsx-pascal-case": 2,
"react/jsx-uses-react": 2,
// Explicitly disabled in case we want to enable them again
"react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 0,
"react/no-direct-mutation-state": 2,
"react/no-multi-comp": [2, { "ignoreStateless": true }],
"react/no-unknown-property": 2,
"react/prefer-es6-class": 2,
"react/prop-types": 2,
"react/react-in-jsx-scope": 2,
"react/self-closing-comp": 2,
"react/sort-comp": 2,
"react/jsx-wrap-multilines": 2
}
}

View File

@@ -1,332 +0,0 @@
const fs = require('fs');
const dirs = fs
.readdirSync('frontend/src', { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name)
.join('|');
const frontendFolder = __dirname;
module.exports = {
parser: '@babel/eslint-parser',
env: {
browser: true,
commonjs: true,
node: true,
es6: true
},
globals: {
expect: false,
chai: false,
sinon: false
},
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
babelOptions: {
configFile: `${frontendFolder}/babel.config.js`
},
ecmaFeatures: {
modules: true,
impliedStrict: true
}
},
plugins: [
'filenames',
'react',
'simple-import-sort',
'import'
],
settings: {
react: {
version: 'detect'
}
},
rules: {
'filenames/match-exported': ['error'],
// ECMAScript 6
'arrow-body-style': [0],
'arrow-parens': ['error', 'always'],
'arrow-spacing': ['error', { before: true, after: true }],
'constructor-super': 'error',
'generator-star-spacing': 'off',
'no-class-assign': 'error',
'no-confusing-arrow': 'error',
'no-const-assign': 'error',
'no-dupe-class-members': 'error',
'no-duplicate-imports': 'error',
'no-new-symbol': 'error',
'no-this-before-super': 'error',
'no-useless-escape': 'error',
'no-useless-computed-key': 'error',
'no-useless-constructor': 'error',
'no-var': 'warn',
'object-shorthand': ['error', 'properties'],
'prefer-arrow-callback': 'error',
'prefer-const': 'warn',
'prefer-reflect': 'off',
'prefer-rest-params': 'off',
'prefer-spread': 'warn',
'prefer-template': 'error',
'require-yield': 'off',
'template-curly-spacing': ['error', 'never'],
'yield-star-spacing': 'off',
// Possible Errors
'comma-dangle': 'error',
'no-cond-assign': 'error',
'no-console': 'off',
'no-constant-condition': 'warn',
'no-control-regex': 'error',
'no-debugger': 'off',
'no-dupe-args': 'error',
'no-dupe-keys': 'error',
'no-duplicate-case': 'error',
'no-empty': 'warn',
'no-empty-character-class': 'error',
'no-ex-assign': 'error',
'no-extra-boolean-cast': 'error',
'no-extra-parens': ['error', 'functions'],
'no-extra-semi': 'error',
'no-func-assign': 'error',
'no-inner-declarations': 'error',
'no-invalid-regexp': 'error',
'no-irregular-whitespace': 'error',
'no-negated-in-lhs': 'error',
'no-obj-calls': 'error',
'no-regex-spaces': 'error',
'no-sparse-arrays': 'error',
'no-unexpected-multiline': 'error',
'no-unreachable': 'warn',
'no-unsafe-finally': 'error',
'use-isnan': 'error',
'valid-jsdoc': 'off',
'valid-typeof': 'error',
// Best Practices
'accessor-pairs': 'off',
'array-callback-return': 'warn',
'block-scoped-var': 'warn',
'consistent-return': 'off',
curly: 'error',
'default-case': 'error',
'dot-location': ['error', 'property'],
'dot-notation': 'error',
eqeqeq: ['error', 'smart'],
'guard-for-in': 'error',
'no-alert': 'warn',
'no-caller': 'error',
'no-case-declarations': 'error',
'no-div-regex': 'error',
'no-else-return': 'error',
'no-empty-function': ['error', { allow: ['arrowFunctions'] }],
'no-empty-pattern': 'error',
'no-eval': 'error',
'no-extend-native': 'error',
'no-extra-bind': 'error',
'no-fallthrough': 'error',
'no-floating-decimal': 'error',
'no-implicit-coercion': ['error', {
boolean: false,
number: true,
string: true,
allow: [/* "!!", "~", "*", "+" */]
}],
'no-implicit-globals': 'error',
'no-implied-eval': 'error',
'no-invalid-this': 'off',
'no-iterator': 'error',
'no-labels': 'error',
'no-lone-blocks': 'error',
'no-loop-func': 'error',
'no-magic-numbers': ['off', { ignoreArrayIndexes: true, ignore: [0, 1] }],
'no-multi-spaces': 'error',
'no-multi-str': 'error',
'no-native-reassign': ['error', { exceptions: ['console'] }],
'no-new': 'off',
'no-new-func': 'error',
'no-new-wrappers': 'error',
'no-octal': 'error',
'no-octal-escape': 'error',
'no-param-reassign': 'off',
'no-process-env': 'off',
'no-proto': 'error',
'no-redeclare': 'error',
'no-return-assign': 'warn',
'no-script-url': 'error',
'no-self-assign': 'error',
'no-self-compare': 'error',
'no-sequences': 'error',
'no-throw-literal': 'error',
'no-unmodified-loop-condition': 'error',
'no-unused-expressions': 'error',
'no-unused-labels': 'error',
'no-useless-call': 'error',
'no-useless-concat': 'error',
'no-void': 'error',
'no-warning-comments': 'off',
'no-with': 'error',
radix: ['error', 'as-needed'],
'vars-on-top': 'off',
'wrap-iife': ['error', 'inside'],
yoda: 'error',
// Strict Mode
strict: ['error', 'never'],
// Variables
'init-declarations': ['error', 'always'],
'no-catch-shadow': 'error',
'no-delete-var': 'error',
'no-label-var': 'error',
'no-restricted-globals': 'off',
'no-shadow': 'error',
'no-shadow-restricted-names': 'error',
'no-undef': 'error',
'no-undef-init': 'off',
'no-undefined': 'off',
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
'no-use-before-define': 'error',
// Node.js and CommonJS
'callback-return': 'warn',
'global-require': 'error',
'handle-callback-err': 'warn',
'no-mixed-requires': 'error',
'no-new-require': 'error',
'no-path-concat': 'error',
'no-process-exit': 'error',
// Stylistic Issues
'array-bracket-spacing': ['error', 'never'],
'block-spacing': ['error', 'always'],
'brace-style': ['error', '1tbs', { allowSingleLine: false }],
camelcase: 'off',
'comma-spacing': ['error', { before: false, after: true }],
'comma-style': ['error', 'last'],
'computed-property-spacing': ['error', 'never'],
'consistent-this': ['error', 'self'],
'eol-last': 'error',
'func-names': 'off',
'func-style': ['error', 'declaration'],
indent: ['error', 2, { SwitchCase: 1 }],
'key-spacing': ['error', { beforeColon: false, afterColon: true }],
'keyword-spacing': ['error', { before: true, after: true }],
'lines-around-comment': ['error', { beforeBlockComment: true, afterBlockComment: false }],
'max-depth': ['error', { maximum: 5 }],
'max-nested-callbacks': ['error', 4],
'max-statements': 'off',
'max-statements-per-line': ['error', { max: 1 }],
'new-cap': ['error', { capIsNewExceptions: ['$.Deferred', 'DragDropContext', 'DragLayer', 'DragSource', 'DropTarget'] }],
'new-parens': 'error',
'newline-after-var': 'off',
'newline-before-return': 'off',
'newline-per-chained-call': 'off',
'no-array-constructor': 'error',
'no-bitwise': 'error',
'no-continue': 'error',
'no-inline-comments': 'off',
'no-lonely-if': 'warn',
'no-mixed-spaces-and-tabs': 'error',
'no-multiple-empty-lines': ['error', { max: 1 }],
'no-negated-condition': 'warn',
'no-nested-ternary': 'error',
'no-new-object': 'error',
'no-plusplus': 'off',
'no-restricted-syntax': 'off',
'no-spaced-func': 'error',
'no-ternary': 'off',
'no-trailing-spaces': 'error',
'no-underscore-dangle': ['error', { allowAfterThis: true }],
'no-unneeded-ternary': 'error',
'no-whitespace-before-property': 'error',
'object-curly-spacing': ['error', 'always'],
'one-var': ['error', 'never'],
'one-var-declaration-per-line': ['error', 'always'],
'operator-assignment': ['off', 'never'],
'operator-linebreak': ['error', 'after'],
'quote-props': ['error', 'as-needed'],
quotes: ['error', 'single'],
'require-jsdoc': 'off',
semi: 'error',
'semi-spacing': ['error', { before: false, after: true }],
'sort-vars': 'off',
'space-before-blocks': ['error', 'always'],
'space-before-function-paren': ['error', 'never'],
'space-in-parens': 'off',
'space-infix-ops': 'off',
'space-unary-ops': 'off',
'spaced-comment': 'error',
'wrap-regex': 'error',
// ImportSort
'simple-import-sort/imports': 'error',
'import/newline-after-import': 'error',
// React
'react/jsx-boolean-value': [2, 'always'],
'react/jsx-uses-vars': 2,
'react/jsx-closing-bracket-location': 2,
'react/jsx-tag-spacing': ['error'],
'react/jsx-curly-spacing': [2, 'never'],
'react/jsx-equals-spacing': [2, 'never'],
'react/jsx-indent-props': [2, 2],
'react/jsx-indent': [2, 2, { indentLogicalExpressions: true }],
'react/jsx-key': 2,
'react/jsx-no-bind': [2, { allowArrowFunctions: true }],
'react/jsx-no-duplicate-props': [2, { ignoreCase: true }],
'react/jsx-max-props-per-line': [2, { maximum: 2 }],
'react/jsx-handler-names': [2, { eventHandlerPrefix: '(on|dispatch)', eventHandlerPropPrefix: 'on' }],
'react/jsx-no-undef': 2,
'react/jsx-pascal-case': 2,
'react/jsx-uses-react': 2,
// Explicitly disabled in case we want to enable them again
'react/no-did-mount-set-state': 0,
'react/no-did-update-set-state': 0,
'react/no-direct-mutation-state': 2,
'react/no-multi-comp': [2, { ignoreStateless: true }],
'react/no-unknown-property': 2,
'react/prefer-es6-class': 2,
'react/prop-types': 2,
'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2,
'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2
},
overrides: [
{
files: ['*.js'],
rules: {
'simple-import-sort/imports': [
'error',
{
groups: [
// Packages
// Absolute Paths
// Relative Paths
// Css
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
]
}
]
}
}
]
};

View File

@@ -24,7 +24,7 @@
"ignoreAtRules": [ "ignoreAtRules": [
"/^add\\-mixin$/", "/^add\\-mixin$/",
"/^define\\-mixin$/" "/^define\\-mixin$/"
] ]
} }
], ],
"at-rule-no-vendor-prefix": true, "at-rule-no-vendor-prefix": true,
@@ -75,7 +75,7 @@
"function-parentheses-newline-inside": "never-multi-line", "function-parentheses-newline-inside": "never-multi-line",
"function-parentheses-space-inside": "never", "function-parentheses-space-inside": "never",
"function-url-quotes": "always", "function-url-quotes": "always",
"function-url-scheme-disallowed-list": [ "function-url-scheme-blacklist": [
"data" "data"
], ],
"function-whitespace-after": "always", "function-whitespace-after": "always",

View File

@@ -1,271 +0,0 @@
const path = require('path');
const webpack = require('webpack');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (env) => {
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = !!env.production;
const isProfiling = isProduction && !!env.profile;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
},
fallback: {
buffer: false,
http: false,
https: false,
url: false,
util: false,
net: false
}
},
output: {
path: distFolder,
publicPath: '/',
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'named',
splitChunks: {
chunks: 'initial',
name: 'vendors'
}
},
performance: {
hints: false
},
plugins: [
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: 'Content/styles.css'
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
}),
new FileManagerPlugin({
events: {
onEnd: {
copy: [
// HTML
{
source: 'frontend/src/*.html',
destination: distFolder
},
// Fonts
{
source: 'frontend/src/Content/Fonts/*.*',
destination: path.join(distFolder, 'Content/Fonts')
},
// Icon Images
{
source: 'frontend/src/Content/Images/Icons/*.*',
destination: path.join(distFolder, 'Content/Images/Icons')
},
// Images
{
source: 'frontend/src/Content/Images/*.*',
destination: path.join(distFolder, 'Content/Images')
},
// Robots
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
}
]
}
}
}),
new LiveReloadPlugin()
],
resolveLoader: {
modules: [
'node_modules',
'frontend/build/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 24096,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|gif|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
limit: 24096,
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
return config;
};

18
frontend/gulp/build.js Normal file
View File

@@ -0,0 +1,18 @@
const gulp = require('gulp');
require('./clean');
require('./copy');
require('./webpack');
gulp.task('build',
gulp.series('clean',
gulp.parallel(
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
)
)
);

8
frontend/gulp/clean.js Normal file
View File

@@ -0,0 +1,8 @@
const gulp = require('gulp');
const del = require('del');
const paths = require('./helpers/paths');
gulp.task('clean', () => {
return del([paths.dest.root]);
});

45
frontend/gulp/copy.js Normal file
View File

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

View File

@@ -0,0 +1,5 @@
require('./build.js');
require('./clean.js');
require('./copy.js');
require('./watch.js');
require('./webpack.js');

View File

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

View File

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

18
frontend/gulp/watch.js Normal file
View File

@@ -0,0 +1,18 @@
const gulp = require('gulp');
const livereload = require('gulp-livereload');
const gulpWatch = require('gulp-watch');
const paths = require('./helpers/paths.js');
require('./copy.js');
require('./webpack.js');
function watch() {
livereload.listen({ start: true });
gulp.task('webpackWatch')();
gulpWatch(paths.src.html, gulp.series('copyHtml'));
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
}
gulp.task('watch', gulp.series('build', watch));

260
frontend/gulp/webpack.js Normal file
View File

@@ -0,0 +1,260 @@
const gulp = require('gulp');
const webpackStream = require('webpack-stream');
const livereload = require('gulp-livereload');
const path = require('path');
const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
'../src/Styles/Variables/dimensions',
'../src/Styles/Variables/fonts',
'../src/Styles/Variables/animations',
'../src/Styles/Variables/zIndexes'
].map(require.resolve);
// Override the way HtmlWebpackPlugin injects the scripts
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
const head = assetTags.head.map((v) => {
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
return this.createHtmlTag(v);
});
const body = assetTags.body.map((v) => {
v.attributes = { src: `/${v.attributes.src}` };
return this.createHtmlTag(v);
});
return html
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
};
const plugins = [
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: path.join('Content', 'styles.css')
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.html',
filename: 'index.html'
})
];
const config = {
mode: isProduction ? 'production' : 'development',
devtool: '#source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
}
},
output: {
path: distFolder,
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
chunkIds: 'named',
splitChunks: {
chunks: 'initial'
}
},
performance: {
hints: false
},
plugins,
resolveLoader: {
modules: [
'node_modules',
'frontend/gulp/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
name: '[name].js'
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
gulp.task('webpack', () => {
return webpackStream(config)
.pipe(gulp.dest('_output/UI'));
});
gulp.task('webpackWatch', () => {
config.watch = true;
return webpackStream(config)
.on('error', errorHandler)
.pipe(gulp.dest('_output/UI'))
.on('error', errorHandler)
.pipe(livereload())
.on('error', errorHandler);
});

View File

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

View File

@@ -1,32 +1,23 @@
const reload = require('require-nocache')(module); const reload = require('require-nocache')(module);
const cssVarsFiles = [ module.exports = (ctx, configPath, options) => {
'./src/Styles/Variables/colors', const config = {
'./src/Styles/Variables/dimensions', plugins: {
'./src/Styles/Variables/fonts', 'postcss-mixins': {
'./src/Styles/Variables/animations', mixinsDir: [
'./src/Styles/Variables/zIndexes' 'frontend/src/Styles/Mixins'
].map(require.resolve); ]
},
'postcss-simple-vars': {
variables: () =>
ctx.options.cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
},
'postcss-color-function': {},
'postcss-nested': {}
}
};
const mixinsFiles = [ return config;
'frontend/src/Styles/Mixins/cover.css', };
'frontend/src/Styles/Mixins/linkOverlay.css',
'frontend/src/Styles/Mixins/scroller.css',
'frontend/src/Styles/Mixins/truncate.css'
];
module.exports = {
plugins: [
['postcss-mixins', {
mixinsFiles
}],
['postcss-simple-vars', {
variables: () =>
cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
}],
'postcss-color-function',
'postcss-nested'
]
};

View File

@@ -0,0 +1,123 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { align, icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
return (
<PageContent title="Blacklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Clear"
iconName={icons.CLEAR}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load blacklist</div>
}
{
isPopulated && !error && !items.length &&
<div>
No history blacklist
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return (
<BlacklistRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBodyConnector>
</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,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};
export default Blacklist;

View File

@@ -0,0 +1,154 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import Blacklist from './Blacklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
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 });
}
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}
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,
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,16 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate'; import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
class BlocklistDetailsModal extends Component { class BlacklistDetailsModal extends Component {
// //
// Render // Render
@@ -40,19 +39,19 @@ class BlocklistDetailsModal extends Component {
<ModalBody> <ModalBody>
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('Name')} title="Name"
data={sourceTitle} data={sourceTitle}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('Protocol')} title="Protocol"
data={protocol} data={protocol}
/> />
{ {
!!message && !!message &&
<DescriptionListItem <DescriptionListItem
title={translate('Indexer')} title="Indexer"
data={indexer} data={indexer}
/> />
} }
@@ -60,7 +59,7 @@ class BlocklistDetailsModal extends Component {
{ {
!!message && !!message &&
<DescriptionListItem <DescriptionListItem
title={translate('Message')} title="Message"
data={message} data={message}
/> />
} }
@@ -69,7 +68,7 @@ class BlocklistDetailsModal extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={onModalClose}> <Button onPress={onModalClose}>
{translate('Close')} Close
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -78,7 +77,7 @@ class BlocklistDetailsModal extends Component {
} }
} }
BlocklistDetailsModal.propTypes = { BlacklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
@@ -87,4 +86,4 @@ BlocklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default BlocklistDetailsModal; export default BlacklistDetailsModal;

View File

@@ -1,20 +1,18 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; 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 TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import MovieQuality from 'Movie/MovieQuality';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import translate from 'Utilities/String/translate'; import BlacklistDetailsModal from './BlacklistDetailsModal';
import BlocklistDetailsModal from './BlocklistDetailsModal'; import styles from './BlacklistRow.css';
import styles from './BlocklistRow.css';
class BlocklistRow extends Component { class BlacklistRow extends Component {
// //
// Lifecycle // Lifecycle
@@ -32,18 +30,17 @@ class BlocklistRow extends Component {
onDetailsPress = () => { onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true }); this.setState({ isDetailsModalOpen: true });
}; }
onDetailsModalClose = () => { onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false }); this.setState({ isDetailsModalOpen: false });
}; }
// //
// Render // Render
render() { render() {
const { const {
id,
movie, movie,
sourceTitle, sourceTitle,
quality, quality,
@@ -53,9 +50,7 @@ class BlocklistRow extends Component {
protocol, protocol,
indexer, indexer,
message, message,
isSelected,
columns, columns,
onSelectedChange,
onRemovePress onRemovePress
} = this.props; } = this.props;
@@ -65,12 +60,6 @@ class BlocklistRow extends Component {
return ( return (
<TableRow> <TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{ {
columns.map((column) => { columns.map((column) => {
const { const {
@@ -166,7 +155,7 @@ class BlocklistRow extends Component {
/> />
<IconButton <IconButton
title={translate('RemoveFromBlocklist')} title="Remove from blacklist"
name={icons.REMOVE} name={icons.REMOVE}
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onRemovePress} onPress={onRemovePress}
@@ -179,7 +168,7 @@ class BlocklistRow extends Component {
}) })
} }
<BlocklistDetailsModal <BlacklistDetailsModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle} sourceTitle={sourceTitle}
protocol={protocol} protocol={protocol}
@@ -193,7 +182,7 @@ class BlocklistRow extends Component {
} }
BlocklistRow.propTypes = { BlacklistRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
movie: PropTypes.object.isRequired, movie: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
@@ -204,10 +193,8 @@ BlocklistRow.propTypes = {
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
indexer: PropTypes.string, indexer: PropTypes.string,
message: PropTypes.string, message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired onRemovePress: PropTypes.func.isRequired
}; };
export default BlocklistRow; export default BlacklistRow;

View File

@@ -1,8 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions'; import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import createMovieSelector from 'Store/Selectors/createMovieSelector'; import createMovieSelector from 'Store/Selectors/createMovieSelector';
import BlocklistRow from './BlocklistRow'; import BlacklistRow from './BlacklistRow';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
@@ -18,9 +18,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onRemovePress() { onRemovePress() {
dispatch(removeBlocklistItem({ id: props.id })); dispatch(removeFromBlacklist({ id: props.id }));
} }
}; };
} }
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow); export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);

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 BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist 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,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blocklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
/>
</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('UnableToLoadBlocklist')}
</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 (
<BlocklistRowConnector
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={translate('RemoveSelected')}
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist')}
confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Blocklist.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,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
};
export default Blocklist;

View File

@@ -1,152 +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 blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blocklistActions,
executeCommand
};
class BlocklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlocklist();
} else {
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlocklist();
};
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage();
};
onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage();
};
onNextPagePress = () => {
this.props.gotoBlocklistNextPage();
};
onLastPagePress = () => {
this.props.gotoBlocklistLastPage();
};
onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page });
};
onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids });
};
onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey });
};
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
};
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
};
//
// Render
render() {
return (
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);

View File

@@ -1,13 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import Link from 'Components/Link/Link';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge'; import formatAge from 'Utilities/Number/formatAge';
import translate from 'Utilities/String/translate'; import Link from 'Components/Link/Link';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
import styles from './HistoryDetails.css'; import styles from './HistoryDetails.css';
function HistoryDetails(props) { function HistoryDetails(props) {
@@ -25,7 +24,6 @@ function HistoryDetails(props) {
releaseGroup, releaseGroup,
nzbInfoUrl, nzbInfoUrl,
downloadClient, downloadClient,
downloadClientName,
downloadId, downloadId,
age, age,
ageHours, ageHours,
@@ -33,20 +31,18 @@ function HistoryDetails(props) {
publishedDate publishedDate
} = data; } = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return ( return (
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('Name')} title="Name"
data={sourceTitle} data={sourceTitle}
/> />
{ {
!!indexer && !!indexer &&
<DescriptionListItem <DescriptionListItem
title={translate('Indexer')} title="Indexer"
data={indexer} data={indexer}
/> />
} }
@@ -55,7 +51,7 @@ function HistoryDetails(props) {
!!releaseGroup && !!releaseGroup &&
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('ReleaseGroup')} title="Release Group"
data={releaseGroup} data={releaseGroup}
/> />
} }
@@ -74,18 +70,17 @@ function HistoryDetails(props) {
} }
{ {
downloadClientNameInfo ? !!downloadClient &&
<DescriptionListItem <DescriptionListItem
title={translate('DownloadClient')} title="Download Client"
data={downloadClientNameInfo} data={downloadClient}
/> : />
null
} }
{ {
!!downloadId && !!downloadId &&
<DescriptionListItem <DescriptionListItem
title={translate('GrabID')} title="Grab ID"
data={downloadId} data={downloadId}
/> />
} }
@@ -93,7 +88,7 @@ function HistoryDetails(props) {
{ {
!!indexer && !!indexer &&
<DescriptionListItem <DescriptionListItem
title={translate('AgeWhenGrabbed')} title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)} data={formatAge(age, ageHours, ageMinutes)}
/> />
} }
@@ -101,7 +96,7 @@ function HistoryDetails(props) {
{ {
!!publishedDate && !!publishedDate &&
<DescriptionListItem <DescriptionListItem
title={translate('PublishedDate')} title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })} data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/> />
} }
@@ -118,14 +113,14 @@ function HistoryDetails(props) {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('Name')} title="Name"
data={sourceTitle} data={sourceTitle}
/> />
{ {
!!message && !!message &&
<DescriptionListItem <DescriptionListItem
title={translate('Message')} title="Message"
data={message} data={message}
/> />
} }
@@ -143,7 +138,7 @@ function HistoryDetails(props) {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('Name')} title="Name"
data={sourceTitle} data={sourceTitle}
/> />
@@ -151,7 +146,7 @@ function HistoryDetails(props) {
!!droppedPath && !!droppedPath &&
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('Source')} title="Source"
data={droppedPath} data={droppedPath}
/> />
} }
@@ -160,7 +155,7 @@ function HistoryDetails(props) {
!!importedPath && !!importedPath &&
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('ImportedTo')} title="Imported To"
data={importedPath} data={importedPath}
/> />
} }
@@ -177,13 +172,13 @@ function HistoryDetails(props) {
switch (reason) { switch (reason) {
case 'Manual': case 'Manual':
reasonMessage = translate('FileWasDeletedByViaUI'); reasonMessage = 'File was deleted by via UI';
break; break;
case 'MissingFromDisk': case 'MissingFromDisk':
reasonMessage = translate('MissingFromDisk'); reasonMessage = 'Radarr was unable to find the file on disk so it was removed';
break; break;
case 'Upgrade': case 'Upgrade':
reasonMessage = translate('FileWasDeletedByUpgrade'); reasonMessage = 'File was deleted to import an upgrade';
break; break;
default: default:
reasonMessage = ''; reasonMessage = '';
@@ -192,12 +187,12 @@ function HistoryDetails(props) {
return ( return (
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('Name')} title="Name"
data={sourceTitle} data={sourceTitle}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('Reason')} title="Reason"
data={reasonMessage} data={reasonMessage}
/> />
</DescriptionList> </DescriptionList>
@@ -215,22 +210,22 @@ function HistoryDetails(props) {
return ( return (
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('SourcePath')} title="Source Path"
data={sourcePath} data={sourcePath}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('SourceRelativePath')} title="Source Relative Path"
data={sourceRelativePath} data={sourceRelativePath}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('DestinationPath')} title="Destination Path"
data={path} data={path}
/> />
<DescriptionListItem <DescriptionListItem
title={translate('DestinationRelativePath')} title="Destination Relative Path"
data={relativePath} data={relativePath}
/> />
</DescriptionList> </DescriptionList>
@@ -246,14 +241,14 @@ function HistoryDetails(props) {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('Name')} title="Name"
data={sourceTitle} data={sourceTitle}
/> />
{ {
!!message && !!message &&
<DescriptionListItem <DescriptionListItem
title={translate('Message')} title="Message"
data={message} data={message}
/> />
} }
@@ -265,7 +260,7 @@ function HistoryDetails(props) {
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
descriptionClassName={styles.description} descriptionClassName={styles.description}
title={translate('Name')} title="Name"
data={sourceTitle} data={sourceTitle}
/> />
</DescriptionList> </DescriptionList>

View File

@@ -1,3 +1,4 @@
import _ from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
@@ -7,10 +8,10 @@ function createMapStateToProps() {
return createSelector( return createSelector(
createUISettingsSelector(), createUISettingsSelector(),
(uiSettings) => { (uiSettings) => {
return { return _.pick(uiSettings, [
shortDateFormat: uiSettings.shortDateFormat, 'shortDateFormat',
timeFormat: uiSettings.timeFormat 'timeFormat'
}; ]);
} }
); );
} }

View File

@@ -1,14 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { kinds } from 'Helpers/Props';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props'; import ModalBody from 'Components/Modal/ModalBody';
import translate from 'Utilities/String/translate'; import ModalFooter from 'Components/Modal/ModalFooter';
import HistoryDetails from './HistoryDetails'; import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css'; import styles from './HistoryDetailsModal.css';
@@ -80,7 +79,7 @@ function HistoryDetailsModal(props) {
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Close')} Close
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>

View File

@@ -1,18 +1,17 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { align, icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager'; import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props'; import PageContent from 'Components/Page/PageContent';
import translate from 'Utilities/String/translate'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import HistoryRowConnector from './HistoryRowConnector'; import HistoryRowConnector from './HistoryRowConnector';
class History extends Component { class History extends Component {
@@ -43,11 +42,11 @@ class History extends Component {
const hasError = error || moviesError; const hasError = error || moviesError;
return ( return (
<PageContent title={translate('History')}> <PageContent title="History">
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
label={translate('Refresh')} label="Refresh"
iconName={icons.REFRESH} iconName={icons.REFRESH}
isSpinning={isFetching} isSpinning={isFetching}
onPress={onFirstPagePress} onPress={onFirstPagePress}
@@ -60,7 +59,7 @@ class History extends Component {
columns={columns} columns={columns}
> >
<PageToolbarButton <PageToolbarButton
label={translate('Options')} label="Options"
iconName={icons.TABLE} iconName={icons.TABLE}
/> />
</TableOptionsModalWrapper> </TableOptionsModalWrapper>
@@ -75,7 +74,7 @@ class History extends Component {
</PageToolbarSection> </PageToolbarSection>
</PageToolbar> </PageToolbar>
<PageContentBody> <PageContentBodyConnector>
{ {
isFetchingAny && !isAllPopulated && isFetchingAny && !isAllPopulated &&
<LoadingIndicator /> <LoadingIndicator />
@@ -83,9 +82,7 @@ class History extends Component {
{ {
!isFetchingAny && hasError && !isFetchingAny && hasError &&
<div> <div>Unable to load history</div>
{translate('UnableToLoadHistory')}
</div>
} }
{ {
@@ -94,7 +91,7 @@ class History extends Component {
isPopulated && !hasError && !items.length && isPopulated && !hasError && !items.length &&
<div> <div>
{translate('NoHistory')} No history found
</div> </div>
} }
@@ -128,7 +125,7 @@ class History extends Component {
/> />
</div> </div>
} }
</PageContentBody> </PageContentBodyConnector>
</PageContent> </PageContent>
); );
} }

View File

@@ -2,9 +2,9 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions'; import * as historyActions from 'Store/Actions/historyActions';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History'; import History from './History';
function createMapStateToProps() { function createMapStateToProps() {
@@ -57,38 +57,38 @@ class HistoryConnector extends Component {
repopulate = () => { repopulate = () => {
this.props.fetchHistory(); this.props.fetchHistory();
}; }
// //
// Listeners // Listeners
onFirstPagePress = () => { onFirstPagePress = () => {
this.props.gotoHistoryFirstPage(); this.props.gotoHistoryFirstPage();
}; }
onPreviousPagePress = () => { onPreviousPagePress = () => {
this.props.gotoHistoryPreviousPage(); this.props.gotoHistoryPreviousPage();
}; }
onNextPagePress = () => { onNextPagePress = () => {
this.props.gotoHistoryNextPage(); this.props.gotoHistoryNextPage();
}; }
onLastPagePress = () => { onLastPagePress = () => {
this.props.gotoHistoryLastPage(); this.props.gotoHistoryLastPage();
}; }
onPageSelect = (page) => { onPageSelect = (page) => {
this.props.gotoHistoryPage({ page }); this.props.gotoHistoryPage({ page });
}; }
onSortPress = (sortKey) => { onSortPress = (sortKey) => {
this.props.setHistorySort({ sortKey }); this.props.setHistorySort({ sortKey });
}; }
onFilterSelect = (selectedFilterKey) => { onFilterSelect = (selectedFilterKey) => {
this.props.setHistoryFilter({ selectedFilterKey }); this.props.setHistoryFilter({ selectedFilterKey });
}; }
onTableOptionChange = (payload) => { onTableOptionChange = (payload) => {
this.props.setHistoryTableOption(payload); this.props.setHistoryTableOption(payload);
@@ -96,7 +96,7 @@ class HistoryConnector extends Component {
if (payload.pageSize) { if (payload.pageSize) {
this.props.gotoHistoryFirstPage(); this.props.gotoHistoryFirstPage();
} }
}; }
// //
// Render // Render

View File

@@ -1,8 +1,8 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import { icons, kinds } from 'Helpers/Props';
import styles from './HistoryEventTypeCell.css'; import styles from './HistoryEventTypeCell.css';
function getIconName(eventType) { function getIconName(eventType) {

View File

@@ -1,16 +1,16 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
import MovieQuality from 'Movie/MovieQuality';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import styles from './HistoryRow.css'; import styles from './HistoryRow.css';
class HistoryRow extends Component { class HistoryRow extends Component {
@@ -41,11 +41,11 @@ class HistoryRow extends Component {
onDetailsPress = () => { onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true }); this.setState({ isDetailsModalOpen: true });
}; }
onDetailsModalClose = () => { onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false }); this.setState({ isDetailsModalOpen: false });
}; }
// //
// Render // Render
@@ -179,16 +179,6 @@ class HistoryRow extends Component {
); );
} }
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') { if (name === 'details') {
return ( return (
<TableRowCell <TableRowCell

View File

@@ -46,7 +46,7 @@ class HistoryRowConnector extends Component {
onMarkAsFailedPress = () => { onMarkAsFailedPress = () => {
this.props.markAsFailed({ id: this.props.id }); this.props.markAsFailed({ id: this.props.id });
}; }
// //
// Render // Render

View File

@@ -1,28 +1,27 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; 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 getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds'; import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
import QueueOptionsConnector from './QueueOptionsConnector'; import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector'; import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
class Queue extends Component { class Queue extends Component {
@@ -32,8 +31,6 @@ class Queue extends Component {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);
this._shouldBlockRefresh = false;
this.state = { this.state = {
allSelected: false, allSelected: false,
allUnselected: false, allUnselected: false,
@@ -45,14 +42,6 @@ class Queue extends Component {
}; };
} }
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
return true;
}
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
items, items,
@@ -90,45 +79,37 @@ class Queue extends Component {
getSelectedIds = () => { getSelectedIds = () => {
return getSelectedIds(this.state.selectedState); return getSelectedIds(this.state.selectedState);
}; }
// //
// Listeners // Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
};
onSelectAllChange = ({ value }) => { onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value)); this.setState(selectAll(this.state.selectedState, value));
}; }
onSelectedChange = ({ id, value, shiftKey = false }) => { onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => { this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey); return toggleSelected(state, this.props.items, id, value, shiftKey);
}); });
}; }
onGrabSelectedPress = () => { onGrabSelectedPress = () => {
this.props.onGrabSelectedPress(this.getSelectedIds()); this.props.onGrabSelectedPress(this.getSelectedIds());
}; }
onRemoveSelectedPress = () => { onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true }, () => { this.setState({ isConfirmRemoveModalOpen: true });
this._shouldBlockRefresh = true; }
});
};
onRemoveSelectedConfirmed = (payload) => { onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload }); this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false }); this.setState({ isConfirmRemoveModalOpen: false });
}; }
onConfirmRemoveModalClose = () => { onConfirmRemoveModalClose = () => {
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false }); this.setState({ isConfirmRemoveModalOpen: false });
}; }
// //
// Render // Render
@@ -167,11 +148,11 @@ class Queue extends Component {
const disableSelectedActions = selectedCount === 0; const disableSelectedActions = selectedCount === 0;
return ( return (
<PageContent title={translate('Queue')}> <PageContent title="Queue">
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
label={translate('Refresh')} label="Refresh"
iconName={icons.REFRESH} iconName={icons.REFRESH}
isSpinning={isRefreshing} isSpinning={isRefreshing}
onPress={onRefreshPress} onPress={onRefreshPress}
@@ -180,7 +161,7 @@ class Queue extends Component {
<PageToolbarSeparator /> <PageToolbarSeparator />
<PageToolbarButton <PageToolbarButton
label={translate('GrabSelected')} label="Grab Selected"
iconName={icons.DOWNLOAD} iconName={icons.DOWNLOAD}
isDisabled={disableSelectedActions || !isPendingSelected} isDisabled={disableSelectedActions || !isPendingSelected}
isSpinning={isGrabbing} isSpinning={isGrabbing}
@@ -188,7 +169,7 @@ class Queue extends Component {
/> />
<PageToolbarButton <PageToolbarButton
label={translate('RemoveSelected')} label="Remove Selected"
iconName={icons.REMOVE} iconName={icons.REMOVE}
isDisabled={disableSelectedActions} isDisabled={disableSelectedActions}
isSpinning={isRemoving} isSpinning={isRemoving}
@@ -205,14 +186,14 @@ class Queue extends Component {
optionsComponent={QueueOptionsConnector} optionsComponent={QueueOptionsConnector}
> >
<PageToolbarButton <PageToolbarButton
label={translate('Options')} label="Options"
iconName={icons.TABLE} iconName={icons.TABLE}
/> />
</TableOptionsModalWrapper> </TableOptionsModalWrapper>
</PageToolbarSection> </PageToolbarSection>
</PageToolbar> </PageToolbar>
<PageContentBody> <PageContentBodyConnector>
{ {
isRefreshing && !isAllPopulated && isRefreshing && !isAllPopulated &&
<LoadingIndicator /> <LoadingIndicator />
@@ -221,14 +202,14 @@ class Queue extends Component {
{ {
!isRefreshing && hasError && !isRefreshing && hasError &&
<div> <div>
{translate('FailedToLoadQueue')} Failed to load Queue
</div> </div>
} }
{ {
isAllPopulated && !hasError && !items.length && isPopulated && !hasError && !items.length &&
<div> <div>
{translate('QueueIsEmpty')} Queue is empty
</div> </div>
} }
@@ -255,7 +236,6 @@ class Queue extends Component {
columns={columns} columns={columns}
{...item} {...item}
onSelectedChange={this.onSelectedChange} onSelectedChange={this.onSelectedChange}
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
/> />
); );
}) })
@@ -270,7 +250,7 @@ class Queue extends Component {
/> />
</div> </div>
} }
</PageContentBody> </PageContentBodyConnector>
<RemoveQueueItemsModal <RemoveQueueItemsModal
isOpen={isConfirmRemoveModalOpen} isOpen={isConfirmRemoveModalOpen}
@@ -282,17 +262,6 @@ class Queue extends Component {
return !!(item && item.movieId); return !!(item && item.movieId);
}) })
)} )}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed} onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose} onModalClose={this.onConfirmRemoveModalClose}
/> />

View File

@@ -2,12 +2,12 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage'; import withCurrentPage from 'Components/withCurrentPage';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import * as queueActions from 'Store/Actions/queueActions'; import * as queueActions from 'Store/Actions/queueActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import * as commandNames from 'Commands/commandNames';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue'; import Queue from './Queue';
function createMapStateToProps() { function createMapStateToProps() {
@@ -43,7 +43,6 @@ class QueueConnector extends Component {
const { const {
useCurrentPage, useCurrentPage,
fetchQueue, fetchQueue,
fetchQueueStatus,
gotoQueueFirstPage gotoQueueFirstPage
} = this.props; } = this.props;
@@ -54,8 +53,6 @@ class QueueConnector extends Component {
} else { } else {
gotoQueueFirstPage(); gotoQueueFirstPage();
} }
fetchQueueStatus();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -77,34 +74,34 @@ class QueueConnector extends Component {
repopulate = () => { repopulate = () => {
this.props.fetchQueue(); this.props.fetchQueue();
}; }
// //
// Listeners // Listeners
onFirstPagePress = () => { onFirstPagePress = () => {
this.props.gotoQueueFirstPage(); this.props.gotoQueueFirstPage();
}; }
onPreviousPagePress = () => { onPreviousPagePress = () => {
this.props.gotoQueuePreviousPage(); this.props.gotoQueuePreviousPage();
}; }
onNextPagePress = () => { onNextPagePress = () => {
this.props.gotoQueueNextPage(); this.props.gotoQueueNextPage();
}; }
onLastPagePress = () => { onLastPagePress = () => {
this.props.gotoQueueLastPage(); this.props.gotoQueueLastPage();
}; }
onPageSelect = (page) => { onPageSelect = (page) => {
this.props.gotoQueuePage({ page }); this.props.gotoQueuePage({ page });
}; }
onSortPress = (sortKey) => { onSortPress = (sortKey) => {
this.props.setQueueSort({ sortKey }); this.props.setQueueSort({ sortKey });
}; }
onTableOptionChange = (payload) => { onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload); this.props.setQueueTableOption(payload);
@@ -112,21 +109,21 @@ class QueueConnector extends Component {
if (payload.pageSize) { if (payload.pageSize) {
this.props.gotoQueueFirstPage(); this.props.gotoQueueFirstPage();
} }
}; }
onRefreshPress = () => { onRefreshPress = () => {
this.props.executeCommand({ this.props.executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS name: commandNames.REFRESH_MONITORED_DOWNLOADS
}); });
}; }
onGrabSelectedPress = (ids) => { onGrabSelectedPress = (ids) => {
this.props.grabQueueItems({ ids }); this.props.grabQueueItems({ ids });
}; }
onRemoveSelectedPress = (payload) => { onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload); this.props.removeQueueItems(payload);
}; }
// //
// Render // Render
@@ -155,7 +152,6 @@ QueueConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired, useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired, fetchQueue: PropTypes.func.isRequired,
fetchQueueStatus: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired, gotoQueueFirstPage: PropTypes.func.isRequired,
gotoQueuePreviousPage: PropTypes.func.isRequired, gotoQueuePreviousPage: PropTypes.func.isRequired,
gotoQueueNextPage: PropTypes.func.isRequired, gotoQueueNextPage: PropTypes.func.isRequired,

View File

@@ -1,9 +1,8 @@
import moment from 'moment'; import moment from 'moment';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import Icon from 'Components/Icon';
function QueueDetails(props) { function QueueDetails(props) {
const { const {
@@ -11,20 +10,20 @@ function QueueDetails(props) {
size, size,
sizeleft, sizeleft,
estimatedCompletionTime, estimatedCompletionTime,
status, status: queueStatus,
trackedDownloadState,
trackedDownloadStatus,
errorMessage, errorMessage,
progressBar progressBar
} = props; } = props;
const progress = size ? (100 - sizeleft / size * 100) : 0; const status = queueStatus.toLowerCase();
const progress = (100 - sizeleft / size * 100);
if (status === 'pending') { if (status === 'pending') {
return ( return (
<Icon <Icon
name={icons.PENDING} name={icons.PENDING}
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])} title={`Release will be processed ${moment(estimatedCompletionTime).fromNow()}`}
/> />
); );
} }
@@ -35,40 +34,12 @@ function QueueDetails(props) {
<Icon <Icon
name={icons.DOWNLOAD} name={icons.DOWNLOAD}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('ImportFailedInterp', [errorMessage])} title={`Import failed: ${errorMessage}`}
/> />
); );
} }
if (trackedDownloadStatus === 'warning') { // TODO: show an icon when download is complete, but not imported yet?
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={translate('UnableToImportCheckLogs')}
/>
);
}
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) { if (errorMessage) {
@@ -76,7 +47,7 @@ function QueueDetails(props) {
<Icon <Icon
name={icons.DOWNLOADING} name={icons.DOWNLOADING}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DownloadFailedInterp', [errorMessage])} title={`Download failed: ${errorMessage}`}
/> />
); );
} }
@@ -86,7 +57,7 @@ function QueueDetails(props) {
<Icon <Icon
name={icons.DOWNLOADING} name={icons.DOWNLOADING}
kind={kinds.DANGER} kind={kinds.DANGER}
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')} title="Download failed: check download client for more details"
/> />
); );
} }
@@ -96,7 +67,7 @@ function QueueDetails(props) {
<Icon <Icon
name={icons.DOWNLOADING} name={icons.DOWNLOADING}
kind={kinds.WARNING} kind={kinds.WARNING}
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')} title="Download warning: check download client for more details"
/> />
); );
} }
@@ -105,7 +76,7 @@ function QueueDetails(props) {
return ( return (
<Icon <Icon
name={icons.DOWNLOADING} name={icons.DOWNLOADING}
title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])} title={`Movie is downloading - ${progress.toFixed(1)}% ${title}`}
/> />
); );
} }
@@ -119,8 +90,6 @@ QueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired, sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string, estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired, status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string, errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired progressBar: PropTypes.node.isRequired
}; };

View File

@@ -1,10 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react'; 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 { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
class QueueOptions extends Component { class QueueOptions extends Component {
@@ -42,7 +41,7 @@ class QueueOptions extends Component {
[name]: value [name]: value
}); });
}); });
}; }
// //
// Render // Render
@@ -55,13 +54,13 @@ class QueueOptions extends Component {
return ( return (
<Fragment> <Fragment>
<FormGroup> <FormGroup>
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel> <FormLabel>Show Unknown Movie Items</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="includeUnknownMovieItems" name="includeUnknownMovieItems"
value={includeUnknownMovieItems} value={includeUnknownMovieItems}
helpText={translate('IncludeUnknownMovieItemsHelpText')} helpText="Show items without a movie in the queue, this could include removed movie, movies or anything else in Radarr's category"
onChange={this.onOptionChange} onChange={this.onOptionChange}
/> />
</FormGroup> </FormGroup>

View File

@@ -1,24 +1,23 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel'; import formatBytes from 'Utilities/Number/formatBytes';
import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
import TableRow from 'Components/Table/TableRow';
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; // import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell'; import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import { icons, kinds } from 'Helpers/Props'; import MovieQuality from 'Movie/MovieQuality';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieFormats from 'Movie/MovieFormats'; import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell'; import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import TimeleftCell from './TimeleftCell'; import TimeleftCell from './TimeleftCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import styles from './QueueRow.css'; import styles from './QueueRow.css';
class QueueRow extends Component { class QueueRow extends Component {
@@ -40,37 +39,24 @@ class QueueRow extends Component {
onRemoveQueueItemPress = () => { onRemoveQueueItemPress = () => {
this.setState({ isRemoveQueueItemModalOpen: true }); this.setState({ isRemoveQueueItemModalOpen: true });
}; }
onRemoveQueueItemModalConfirmed = (blocklist) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist);
onRemoveQueueItemModalConfirmed = (blacklist) => {
this.props.onRemoveQueueItemPress(blacklist);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
}; }
onRemoveQueueItemModalClose = () => { onRemoveQueueItemModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
}; }
onInteractiveImportPress = () => { onInteractiveImportPress = () => {
this.props.onQueueRowModalOpenOrClose(true);
this.setState({ isInteractiveImportModalOpen: true }); this.setState({ isInteractiveImportModalOpen: true });
}; }
onInteractiveImportModalClose = () => { onInteractiveImportModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isInteractiveImportModalOpen: false }); this.setState({ isInteractiveImportModalOpen: false });
}; }
// //
// Render // Render
@@ -308,7 +294,7 @@ class QueueRow extends Component {
} }
<SpinnerIconButton <SpinnerIconButton
title={translate('RemoveFromQueue')} title="Remove from queue"
name={icons.REMOVE} name={icons.REMOVE}
isSpinning={isRemoving} isSpinning={isRemoving}
onPress={this.onRemoveQueueItemPress} onPress={this.onRemoveQueueItemPress}
@@ -332,7 +318,6 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen} isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title} sourceTitle={title}
canIgnore={!!movie} canIgnore={!!movie}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed} onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose} onModalClose={this.onRemoveQueueItemModalClose}
/> />
@@ -373,8 +358,7 @@ QueueRow.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired, onSelectedChange: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired, onGrabPress: PropTypes.func.isRequired,
onRemoveQueueItemPress: PropTypes.func.isRequired, onRemoveQueueItemPress: PropTypes.func.isRequired
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
}; };
QueueRow.defaultProps = { QueueRow.defaultProps = {

View File

@@ -37,11 +37,11 @@ class QueueRowConnector extends Component {
onGrabPress = () => { onGrabPress = () => {
this.props.grabQueueItem({ id: this.props.id }); this.props.grabQueueItem({ id: this.props.id });
}; }
onRemoveQueueItemPress = (payload) => { onRemoveQueueItemPress = (payload) => {
this.props.removeQueueItem({ id: this.props.id, ...payload }); this.props.removeQueueItem({ id: this.props.id, ...payload });
}; }
// //
// Render // Render

View File

@@ -1,10 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './QueueStatusCell.css'; import styles from './QueueStatusCell.css';
function getDetailedPopoverBody(statusMessages) { function getDetailedPopoverBody(statusMessages) {
@@ -50,75 +49,71 @@ function QueueStatusCell(props) {
// status === 'downloading' // status === 'downloading'
let iconName = icons.DOWNLOADING; let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT; let iconKind = kinds.DEFAULT;
let title = translate('Downloading'); let title = '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) { if (hasWarning) {
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
} }
if (status === 'paused') {
iconName = icons.PAUSED;
title = 'Paused';
}
if (status === 'queued') {
iconName = icons.QUEUED;
title = 'Queued';
}
if (status === 'completed') {
iconName = icons.DOWNLOADED;
title = 'Downloaded';
if (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
}
if (trackedDownloadState === 'importing') {
title += ' - Importing';
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
}
}
if (status === 'delay') { if (status === 'delay') {
iconName = icons.PENDING; iconName = icons.PENDING;
title = translate('Pending'); title = 'Pending';
} }
if (status === 'DownloadClientUnavailable') { if (status === 'DownloadClientUnavailable') {
iconName = icons.PENDING; iconName = icons.PENDING;
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
title = `${translate('Pending')} - ${translate('DownloadClientUnavailable')}`; title = 'Pending - Download client is unavailable';
} }
if (status === 'failed') { if (status === 'failed') {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = translate('DownloadFailed'); title = 'Download failed';
} }
if (status === 'warning') { if (status === 'warning') {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING; iconKind = kinds.WARNING;
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails'); title = `Download warning: ${errorMessage || 'check download client for more details'}`;
title = translate('DownloadWarning', [warningMessage]);
} }
if (hasError) { if (hasError) {
if (status === 'completed') { if (status === 'completed') {
iconName = icons.DOWNLOAD; iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = translate('ImportFailed', [sourceTitle]); title = `Import failed: ${sourceTitle}`;
} else { } else {
iconName = icons.DOWNLOADING; iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER; iconKind = kinds.DANGER;
title = translate('DownloadFailed'); title = 'Download failed';
} }
} }
@@ -150,8 +145,8 @@ QueueStatusCell.propTypes = {
}; };
QueueStatusCell.defaultProps = { QueueStatusCell.defaultProps = {
trackedDownloadStatus: translate('Ok'), trackedDownloadStatus: 'Ok',
trackedDownloadState: translate('Downloading') trackedDownloadState: 'Downloading'
}; };
export default QueueStatusCell; export default QueueStatusCell;

View File

@@ -1,16 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody'; import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props'; import ModalBody from 'Components/Modal/ModalBody';
import translate from 'Utilities/String/translate'; import ModalFooter from 'Components/Modal/ModalFooter';
class RemoveQueueItemModal extends Component { class RemoveQueueItemModal extends Component {
@@ -22,7 +21,7 @@ class RemoveQueueItemModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false blacklist: false
}; };
} }
@@ -32,32 +31,32 @@ class RemoveQueueItemModal extends Component {
resetState = function() { resetState = function() {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false blacklist: false
}); });
}; }
// //
// Listeners // Listeners
onRemoveChange = ({ value }) => { onRemoveChange = ({ value }) => {
this.setState({ remove: value }); this.setState({ remove: value });
}; }
onBlocklistChange = ({ value }) => { onBlacklistChange = ({ value }) => {
this.setState({ blocklist: value }); this.setState({ blacklist: value });
}; }
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
const state = this.state; const state = this.state;
this.resetState(); this.resetState();
this.props.onRemovePress(state); this.props.onRemovePress(state);
}; }
onModalClose = () => { onModalClose = () => {
this.resetState(); this.resetState();
this.props.onModalClose(); this.props.onModalClose();
}; }
// //
// Render // Render
@@ -66,11 +65,10 @@ class RemoveQueueItemModal extends Component {
const { const {
isOpen, isOpen,
sourceTitle, sourceTitle,
canIgnore, canIgnore
isPending
} = this.props; } = this.props;
const { remove, blocklist } = this.state; const { remove, blacklist } = this.state;
return ( return (
<Modal <Modal
@@ -82,39 +80,35 @@ class RemoveQueueItemModal extends Component {
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
> >
<ModalHeader> <ModalHeader>
{translate('Remove')} - {sourceTitle} Remove - {sourceTitle}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
{translate('RemoveFromQueueText', [sourceTitle])} Are you sure you want to remove '{sourceTitle}' from the queue?
</div> </div>
{
isPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup> <FormGroup>
<FormLabel>{translate('BlocklistRelease')}</FormLabel> <FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="remove"
value={blocklist} value={remove}
helpText={translate('BlocklistHelpText')} helpTextWarning="Removing will remove the download and the file(s) from the download client."
onChange={this.onBlocklistChange} isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText="Starts a search for this movie again and prevents this release from being grabbed again"
onChange={this.onBlacklistChange}
/> />
</FormGroup> </FormGroup>
@@ -122,7 +116,7 @@ class RemoveQueueItemModal extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={this.onModalClose}> <Button onPress={this.onModalClose}>
{translate('Close')} Close
</Button> </Button>
<Button <Button
@@ -142,7 +136,6 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired, canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired, onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@@ -1,16 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup'; import { inputTypes, kinds, sizes } from 'Helpers/Props';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody'; import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props'; import ModalBody from 'Components/Modal/ModalBody';
import translate from 'Utilities/String/translate'; import ModalFooter from 'Components/Modal/ModalFooter';
import styles from './RemoveQueueItemsModal.css'; import styles from './RemoveQueueItemsModal.css';
class RemoveQueueItemsModal extends Component { class RemoveQueueItemsModal extends Component {
@@ -23,42 +22,42 @@ class RemoveQueueItemsModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false blacklist: false
}; };
} }
// //
// Control // Control
resetState = function() { resetState = function() {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false blacklist: false
}); });
}; }
// //
// Listeners // Listeners
onRemoveChange = ({ value }) => { onRemoveChange = ({ value }) => {
this.setState({ remove: value }); this.setState({ remove: value });
}; }
onBlocklistChange = ({ value }) => { onBlacklistChange = ({ value }) => {
this.setState({ blocklist: value }); this.setState({ blacklist: value });
}; }
onRemoveConfirmed = () => { onRemoveConfirmed = () => {
const state = this.state; const state = this.state;
this.resetState(); this.resetState();
this.props.onRemovePress(state); this.props.onRemovePress(state);
}; }
onModalClose = () => { onModalClose = () => {
this.resetState(); this.resetState();
this.props.onModalClose(); this.props.onModalClose();
}; }
// //
// Render // Render
@@ -67,11 +66,10 @@ class RemoveQueueItemsModal extends Component {
const { const {
isOpen, isOpen,
selectedCount, selectedCount,
canIgnore, canIgnore
allPending
} = this.props; } = this.props;
const { remove, blocklist } = this.state; const { remove, blacklist } = this.state;
return ( return (
<Modal <Modal
@@ -83,42 +81,38 @@ class RemoveQueueItemsModal extends Component {
onModalClose={this.onModalClose} onModalClose={this.onModalClose}
> >
<ModalHeader> <ModalHeader>
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')} Remove Selected Item{selectedCount > 1 ? 's' : ''}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div className={styles.message}> <div className={styles.message}>
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')} Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div> </div>
{ <FormGroup>
allPending ? <FormLabel>Remove From Download Client</FormLabel>
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="remove" name="remove"
value={remove} value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')} helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore} isDisabled={!canIgnore}
onChange={this.onRemoveChange} onChange={this.onRemoveChange}
/> />
</FormGroup> </FormGroup>
}
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')} Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="blacklist"
value={blocklist} value={blacklist}
helpText={translate('BlocklistHelpText')} helpText="Prevents Radarr from automatically grabbing this movie again"
onChange={this.onBlocklistChange} onChange={this.onBlacklistChange}
/> />
</FormGroup> </FormGroup>
@@ -126,7 +120,7 @@ class RemoveQueueItemsModal extends Component {
<ModalFooter> <ModalFooter>
<Button onPress={this.onModalClose}> <Button onPress={this.onModalClose}>
{translate('Close')} Close
</Button> </Button>
<Button <Button
@@ -146,7 +140,6 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired, selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired, canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired, onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };

View File

@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
import { fetchQueueStatus } from 'Store/Actions/queueActions'; import { fetchQueueStatus } from 'Store/Actions/queueActions';
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(

View File

@@ -1,11 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTime from 'Utilities/Date/formatTime'; import formatTime from 'Utilities/Date/formatTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan'; import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import styles from './TimeleftCell.css'; import styles from './TimeleftCell.css';
function TimeleftCell(props) { function TimeleftCell(props) {
@@ -20,35 +19,35 @@ function TimeleftCell(props) {
timeFormat timeFormat
} = props; } = props;
if (status === 'delay') { if (status === 'Delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
<TableRowCell <TableRowCell
className={styles.timeleft} className={styles.timeleft}
title={translate('DelayingDownloadUntilInterp', [date, time])} title={`Delaying download until ${date} at ${time}`}
> >
- -
</TableRowCell> </TableRowCell>
); );
} }
if (status === 'downloadClientUnavailable') { if (status === 'DownloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates); const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true }); const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return ( return (
<TableRowCell <TableRowCell
className={styles.timeleft} className={styles.timeleft}
title={translate('RetryingDownloadInterp', [date, time])} title={`Retrying download ${date} at ${time}`}
> >
- -
</TableRowCell> </TableRowCell>
); );
} }
if (!timeleft || status === 'completed' || status === 'failed') { if (!timeleft) {
return ( return (
<TableRowCell className={styles.timeleft}> <TableRowCell className={styles.timeleft}>
- -

View File

@@ -2,32 +2,25 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import createAddMovieClientSideCollectionItemsSelector from 'Store/Selectors/createAddMovieClientSideCollectionItemsSelector';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { addImportExclusions, addMovies, clearAddMovie, fetchDiscoverMovies, setListMovieFilter, setListMovieSort, setListMovieTableOption, setListMovieView } from 'Store/Actions/discoverMovieActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
import scrollPositions from 'Store/scrollPositions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createDiscoverMovieClientSideCollectionItemsSelector from 'Store/Selectors/createDiscoverMovieClientSideCollectionItemsSelector'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchDiscoverMovies, clearAddMovie, setListMovieSort, setListMovieFilter, setListMovieView, setListMovieTableOption } from 'Store/Actions/addMovieActions';
import scrollPositions from 'Store/scrollPositions';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import DiscoverMovie from './DiscoverMovie'; import withScrollPosition from 'Components/withScrollPosition';
import AddListMovie from './AddListMovie';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createDiscoverMovieClientSideCollectionItemsSelector('discoverMovie'), createAddMovieClientSideCollectionItemsSelector('addMovie'),
createCommandExecutingSelector(commandNames.IMPORT_LIST_SYNC),
createDimensionsSelector(), createDimensionsSelector(),
( (
movies, movies,
isSyncingLists,
dimensionsState dimensionsState
) => { ) => {
return { return {
...movies, ...movies,
isSyncingLists,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
@@ -40,10 +33,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(fetchRootFolders()); dispatch(fetchRootFolders());
}, },
dispatchFetchImportExclusions() {
dispatch(fetchImportExclusions());
},
dispatchClearListMovie() { dispatchClearListMovie() {
dispatch(clearAddMovie()); dispatch(clearAddMovie());
}, },
@@ -66,25 +55,11 @@ function createMapDispatchToProps(dispatch, props) {
dispatchSetListMovieView(view) { dispatchSetListMovieView(view) {
dispatch(setListMovieView({ view })); dispatch(setListMovieView({ view }));
},
dispatchAddMovies(ids, addOptions) {
dispatch(addMovies({ ids, addOptions }));
},
dispatchAddImportExclusions(exclusions) {
dispatch(addImportExclusions(exclusions));
},
onImportListSyncPress() {
dispatch(executeCommand({
name: commandNames.IMPORT_LIST_SYNC
}));
} }
}; };
} }
class DiscoverMovieConnector extends Component { class AddDiscoverMovieConnector extends Component {
// //
// Lifecycle // Lifecycle
@@ -92,7 +67,6 @@ class DiscoverMovieConnector extends Component {
componentDidMount() { componentDidMount() {
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders(); this.props.dispatchFetchRootFolders();
this.props.dispatchFetchImportExclusions();
this.props.dispatchFetchListMovies(); this.props.dispatchFetchListMovies();
} }
@@ -106,50 +80,37 @@ class DiscoverMovieConnector extends Component {
onViewSelect = (view) => { onViewSelect = (view) => {
this.props.dispatchSetListMovieView(view); this.props.dispatchSetListMovieView(view);
}; }
onScroll = ({ scrollTop }) => { onScroll = ({ scrollTop }) => {
scrollPositions.discoverMovie = scrollTop; scrollPositions.addMovie = scrollTop;
}; }
onAddMoviesPress = ({ ids, addOptions }) => {
this.props.dispatchAddMovies(ids, addOptions);
};
onExcludeMoviesPress =({ ids }) => {
this.props.dispatchAddImportExclusions({ ids });
};
// //
// Render // Render
render() { render() {
return ( return (
<DiscoverMovie <AddListMovie
{...this.props} {...this.props}
onViewSelect={this.onViewSelect} onViewSelect={this.onViewSelect}
onScroll={this.onScroll} onScroll={this.onScroll}
onAddMoviesPress={this.onAddMoviesPress} onSaveSelected={this.onSaveSelected}
onExcludeMoviesPress={this.onExcludeMoviesPress}
onSyncListsPress={this.onSyncListsPress}
/> />
); );
} }
} }
DiscoverMovieConnector.propTypes = { AddDiscoverMovieConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
dispatchFetchImportExclusions: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired, dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchListMovies: PropTypes.func.isRequired, dispatchFetchListMovies: PropTypes.func.isRequired,
dispatchClearListMovie: PropTypes.func.isRequired, dispatchClearListMovie: PropTypes.func.isRequired,
dispatchSetListMovieView: PropTypes.func.isRequired, dispatchSetListMovieView: PropTypes.func.isRequired
dispatchAddMovies: PropTypes.func.isRequired,
dispatchAddImportExclusions: PropTypes.func.isRequired
}; };
export default withScrollPosition( export default withScrollPosition(
connect(createMapStateToProps, createMapDispatchToProps)(DiscoverMovieConnector), connect(createMapStateToProps, createMapDispatchToProps)(AddDiscoverMovieConnector),
'discoverMovie' 'addMovie'
); );

View File

@@ -1,47 +1,40 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItems';
import { align, icons, sortDirections } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageJumpBar from 'Components/Page/PageJumpBar'; import PageJumpBar from 'Components/Page/PageJumpBar';
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 TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import AddListMovieTableConnector from './Table/AddListMovieTableConnector';
import AddListMoviePosterOptionsModal from './Posters/Options/AddListMoviePosterOptionsModal';
import AddListMoviePostersConnector from './Posters/AddListMoviePostersConnector';
import AddListMovieOverviewOptionsModal from './Overview/Options/AddListMovieOverviewOptionsModal';
import AddListMovieOverviewsConnector from './Overview/AddListMovieOverviewsConnector';
import AddListMovieFilterMenu from 'AddMovie/AddListMovie/Menus/AddListMovieFilterMenu';
import AddListMovieSortMenu from 'AddMovie/AddListMovie/Menus/AddListMovieSortMenu';
import AddListMovieViewMenu from 'AddMovie/AddListMovie/Menus/AddListMovieViewMenu';
import styles from 'Movie/Index/MovieIndex.css'; import styles from 'Movie/Index/MovieIndex.css';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
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 DiscoverMovieFooterConnector from './DiscoverMovieFooterConnector';
import DiscoverMovieFilterMenu from './Menus/DiscoverMovieFilterMenu';
import DiscoverMovieSortMenu from './Menus/DiscoverMovieSortMenu';
import DiscoverMovieViewMenu from './Menus/DiscoverMovieViewMenu';
import NoDiscoverMovie from './NoDiscoverMovie';
import DiscoverMovieOverviewsConnector from './Overview/DiscoverMovieOverviewsConnector';
import DiscoverMovieOverviewOptionsModal from './Overview/Options/DiscoverMovieOverviewOptionsModal';
import DiscoverMoviePostersConnector from './Posters/DiscoverMoviePostersConnector';
import DiscoverMoviePosterOptionsModal from './Posters/Options/DiscoverMoviePosterOptionsModal';
import DiscoverMovieTableConnector from './Table/DiscoverMovieTableConnector';
import DiscoverMovieTableOptionsConnector from './Table/DiscoverMovieTableOptionsConnector';
function getViewComponent(view) { function getViewComponent(view) {
if (view === 'posters') { if (view === 'posters') {
return DiscoverMoviePostersConnector; return AddListMoviePostersConnector;
} }
if (view === 'overview') { if (view === 'overview') {
return DiscoverMovieOverviewsConnector; return AddListMovieOverviewsConnector;
} }
return DiscoverMovieTableConnector; return AddListMovieTableConnector;
} }
class DiscoverMovie extends Component { class AddListMovie extends Component {
// //
// Lifecycle // Lifecycle
@@ -57,16 +50,12 @@ class DiscoverMovie extends Component {
isOverviewOptionsModalOpen: false, isOverviewOptionsModalOpen: false,
isConfirmSearchModalOpen: false, isConfirmSearchModalOpen: false,
searchType: null, searchType: null,
allSelected: false, lastToggled: null
allUnselected: false,
lastToggled: null,
selectedState: {}
}; };
} }
componentDidMount() { componentDidMount() {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -81,7 +70,6 @@ class DiscoverMovie extends Component {
hasDifferentItemsOrOrder(prevProps.items, items) hasDifferentItemsOrOrder(prevProps.items, items)
) { ) {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
if (this.state.jumpToCharacter != null) { if (this.state.jumpToCharacter != null) {
@@ -94,48 +82,6 @@ class DiscoverMovie extends Component {
setScrollerRef = (ref) => { setScrollerRef = (ref) => {
this.setState({ scroller: ref }); this.setState({ scroller: ref });
};
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
};
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((movie) => {
const isItemSelected = selectedState[movie.tmdbId];
if (isItemSelected) {
newSelectedState[movie.tmdbId] = isItemSelected;
} else {
newSelectedState[movie.tmdbId] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
} }
setJumpBarItems() { setJumpBarItems() {
@@ -187,49 +133,23 @@ class DiscoverMovie extends Component {
onPosterOptionsPress = () => { onPosterOptionsPress = () => {
this.setState({ isPosterOptionsModalOpen: true }); this.setState({ isPosterOptionsModalOpen: true });
}; }
onPosterOptionsModalClose = () => { onPosterOptionsModalClose = () => {
this.setState({ isPosterOptionsModalOpen: false }); this.setState({ isPosterOptionsModalOpen: false });
}; }
onOverviewOptionsPress = () => { onOverviewOptionsPress = () => {
this.setState({ isOverviewOptionsModalOpen: true }); this.setState({ isOverviewOptionsModalOpen: true });
}; }
onOverviewOptionsModalClose = () => { onOverviewOptionsModalClose = () => {
this.setState({ isOverviewOptionsModalOpen: false }); this.setState({ isOverviewOptionsModalOpen: false });
}; }
onJumpBarItemPress = (jumpToCharacter) => { onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter }); this.setState({ jumpToCharacter });
}; }
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
};
onImportListSyncPress = () => {
this.props.onImportListSyncPress();
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey, 'tmdbId');
});
};
onAddMoviesPress = ({ addOptions }) => {
this.props.onAddMoviesPress({ ids: this.getSelectedIds(), addOptions });
};
onExcludeMoviesPress = () => {
this.props.onExcludeMoviesPress({ ids: this.getSelectedIds() });
};
// //
// Render // Render
@@ -252,8 +172,6 @@ class DiscoverMovie extends Component {
onFilterSelect, onFilterSelect,
onViewSelect, onViewSelect,
onScroll, onScroll,
onAddMoviesPress,
isSyncingLists,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -262,14 +180,9 @@ class DiscoverMovie extends Component {
jumpBarItems, jumpBarItems,
jumpToCharacter, jumpToCharacter,
isPosterOptionsModalOpen, isPosterOptionsModalOpen,
isOverviewOptionsModalOpen, isOverviewOptionsModalOpen
selectedState,
allSelected,
allUnselected
} = this.state; } = this.state;
const selectedMovieIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view); const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller); const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoMovie = !totalItems; const hasNoMovie = !totalItems;
@@ -277,22 +190,6 @@ class DiscoverMovie extends Component {
return ( return (
<PageContent> <PageContent>
<PageToolbar> <PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RefreshLists')}
iconName={icons.REFRESH}
isSpinning={isSyncingLists}
isDisabled={hasNoMovie}
onPress={this.onImportListSyncPress}
/>
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoMovie}
onPress={this.onSelectAllPress}
/>
</PageToolbarSection>
<PageToolbarSection <PageToolbarSection
alignContent={align.RIGHT} alignContent={align.RIGHT}
collapseButtons={false} collapseButtons={false}
@@ -302,10 +199,9 @@ class DiscoverMovie extends Component {
<TableOptionsModalWrapper <TableOptionsModalWrapper
{...otherProps} {...otherProps}
columns={columns} columns={columns}
optionsComponent={DiscoverMovieTableOptionsConnector}
> >
<PageToolbarButton <PageToolbarButton
label={translate('Options')} label="Options"
iconName={icons.TABLE} iconName={icons.TABLE}
/> />
</TableOptionsModalWrapper> : </TableOptionsModalWrapper> :
@@ -315,8 +211,9 @@ class DiscoverMovie extends Component {
{ {
view === 'posters' ? view === 'posters' ?
<PageToolbarButton <PageToolbarButton
label={translate('Options')} label="Options"
iconName={icons.POSTER} iconName={icons.POSTER}
isDisabled={hasNoMovie}
onPress={this.onPosterOptionsPress} onPress={this.onPosterOptionsPress}
/> : /> :
null null
@@ -325,8 +222,9 @@ class DiscoverMovie extends Component {
{ {
view === 'overview' ? view === 'overview' ?
<PageToolbarButton <PageToolbarButton
label={translate('Options')} label="Options"
iconName={icons.OVERVIEW} iconName={icons.OVERVIEW}
isDisabled={hasNoMovie}
onPress={this.onOverviewOptionsPress} onPress={this.onOverviewOptionsPress}
/> : /> :
null null
@@ -337,20 +235,20 @@ class DiscoverMovie extends Component {
<PageToolbarSeparator /> <PageToolbarSeparator />
} }
<DiscoverMovieViewMenu <AddListMovieViewMenu
view={view} view={view}
isDisabled={hasNoMovie} isDisabled={hasNoMovie}
onViewSelect={onViewSelect} onViewSelect={onViewSelect}
/> />
<DiscoverMovieSortMenu <AddListMovieSortMenu
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
isDisabled={hasNoMovie} isDisabled={hasNoMovie}
onSortSelect={onSortSelect} onSortSelect={onSortSelect}
/> />
<DiscoverMovieFilterMenu <AddListMovieFilterMenu
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={customFilters} customFilters={customFilters}
@@ -361,7 +259,7 @@ class DiscoverMovie extends Component {
</PageToolbar> </PageToolbar>
<div className={styles.pageContentBodyWrapper}> <div className={styles.pageContentBodyWrapper}>
<PageContentBody <PageContentBodyConnector
registerScroller={this.setScrollerRef} registerScroller={this.setScrollerRef}
className={styles.contentBody} className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]} innerClassName={styles[`${view}InnerContentBody`]}
@@ -374,9 +272,7 @@ class DiscoverMovie extends Component {
{ {
!isFetching && !!error && !isFetching && !!error &&
<div> <div>Unable to load movies</div>
{translate('UnableToLoadMovies')}
</div>
} }
{ {
@@ -389,11 +285,6 @@ class DiscoverMovie extends Component {
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter} jumpToCharacter={jumpToCharacter}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps} {...otherProps}
/> />
</div> </div>
@@ -401,9 +292,11 @@ class DiscoverMovie extends Component {
{ {
!error && isPopulated && !items.length && !error && isPopulated && !items.length &&
<NoDiscoverMovie totalItems={totalItems} /> <div className={styles.message}>
<div className={styles.noResults}>Couldn't find any results</div>
</div>
} }
</PageContentBody> </PageContentBodyConnector>
{ {
isLoaded && !!jumpBarItems.order.length && isLoaded && !!jumpBarItems.order.length &&
@@ -414,21 +307,12 @@ class DiscoverMovie extends Component {
} }
</div> </div>
{ <AddListMoviePosterOptionsModal
isLoaded &&
<DiscoverMovieFooterConnector
selectedIds={selectedMovieIds}
onAddMoviesPress={this.onAddMoviesPress}
onExcludeMoviesPress={this.onExcludeMoviesPress}
/>
}
<DiscoverMoviePosterOptionsModal
isOpen={isPosterOptionsModalOpen} isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose} onModalClose={this.onPosterOptionsModalClose}
/> />
<DiscoverMovieOverviewOptionsModal <AddListMovieOverviewOptionsModal
isOpen={isOverviewOptionsModalOpen} isOpen={isOverviewOptionsModalOpen}
onModalClose={this.onOverviewOptionsModalClose} onModalClose={this.onOverviewOptionsModalClose}
/> />
@@ -437,7 +321,7 @@ class DiscoverMovie extends Component {
} }
} }
DiscoverMovie.propTypes = { AddListMovie.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
@@ -450,15 +334,11 @@ DiscoverMovie.propTypes = {
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
isSyncingLists: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired, onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired, onViewSelect: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired, onScroll: PropTypes.func.isRequired
onAddMoviesPress: PropTypes.func.isRequired,
onExcludeMoviesPress: PropTypes.func.isRequired,
onImportListSyncPress: PropTypes.func.isRequired
}; };
export default DiscoverMovie; export default AddListMovie;

View File

@@ -0,0 +1,113 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAddMovieClientSideCollectionItemsSelector from 'Store/Selectors/createAddMovieClientSideCollectionItemsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { fetchListMovies, clearAddMovie, setListMovieSort, setListMovieFilter, setListMovieView, setListMovieTableOption } from 'Store/Actions/addMovieActions';
import scrollPositions from 'Store/scrollPositions';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withScrollPosition from 'Components/withScrollPosition';
import AddListMovie from './AddListMovie';
function createMapStateToProps() {
return createSelector(
createAddMovieClientSideCollectionItemsSelector('addMovie'),
createDimensionsSelector(),
(
movies,
dimensionsState
) => {
return {
...movies,
isSmallScreen: dimensionsState.isSmallScreen
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchRootFolders() {
dispatch(fetchRootFolders());
},
dispatchFetchListMovies() {
dispatch(fetchListMovies());
},
onTableOptionChange(payload) {
dispatch(setListMovieTableOption(payload));
},
onSortSelect(sortKey) {
dispatch(setListMovieSort({ sortKey }));
},
onFilterSelect(selectedFilterKey) {
dispatch(setListMovieFilter({ selectedFilterKey }));
},
dispatchSetListMovieView(view) {
dispatch(setListMovieView({ view }));
},
dispatchClearListMovie() {
dispatch(clearAddMovie());
}
};
}
class AddListMovieConnector extends Component {
componentDidMount() {
registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders();
this.props.dispatchFetchListMovies();
}
componentWillUnmount() {
this.props.dispatchClearListMovie();
unregisterPagePopulator(this.repopulate);
}
//
// Listeners
onViewSelect = (view) => {
this.props.dispatchSetListMovieView(view);
}
onScroll = ({ scrollTop }) => {
scrollPositions.addMovie = scrollTop;
}
//
// Render
render() {
return (
<AddListMovie
{...this.props}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
/>
);
}
}
AddListMovieConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchListMovies: PropTypes.func.isRequired,
dispatchClearListMovie: PropTypes.func.isRequired,
dispatchSetListMovieView: PropTypes.func.isRequired
};
export default withScrollPosition(
connect(createMapStateToProps, createMapDispatchToProps)(AddListMovieConnector),
'addMovie'
);

View File

@@ -1,17 +1,17 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { setListMovieFilter } from 'Store/Actions/addMovieActions';
import FilterModal from 'Components/Filter/FilterModal'; import FilterModal from 'Components/Filter/FilterModal';
import { setListMovieFilter } from 'Store/Actions/discoverMovieActions';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.discoverMovie.items, (state) => state.addMovie.items,
(state) => state.discoverMovie.filterBuilderProps, (state) => state.addMovie.filterBuilderProps,
(sectionItems, filterBuilderProps) => { (sectionItems, filterBuilderProps) => {
return { return {
sectionItems, sectionItems,
filterBuilderProps, filterBuilderProps,
customFilterType: 'discoverMovie' customFilterType: 'addMovie'
}; };
} }
); );

View File

@@ -2,13 +2,16 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createDiscoverMovieSelector from 'Store/Selectors/createDiscoverMovieSelector'; import createAddListMovieSelector from 'Store/Selectors/createAddListMovieSelector';
import createMovieQualityProfileSelector from 'Store/Selectors/createMovieQualityProfileSelector';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createDiscoverMovieSelector(), createAddListMovieSelector(),
createMovieQualityProfileSelector(),
( (
movie movie,
qualityProfile
) => { ) => {
// If a movie is deleted this selector may fire before the parent // If a movie is deleted this selector may fire before the parent
@@ -21,13 +24,17 @@ function createMapStateToProps() {
} }
return { return {
...movie ...movie,
qualityProfile
}; };
} }
); );
} }
class DiscoverMovieItemConnector extends Component { const mapDispatchToProps = {
};
class AddListMovieItemConnector extends Component {
// //
// Render // Render
@@ -52,9 +59,9 @@ class DiscoverMovieItemConnector extends Component {
} }
} }
DiscoverMovieItemConnector.propTypes = { AddListMovieItemConnector.propTypes = {
tmdbId: PropTypes.number, tmdbId: PropTypes.number,
component: PropTypes.elementType.isRequired component: PropTypes.elementType.isRequired
}; };
export default connect(createMapStateToProps)(DiscoverMovieItemConnector); export default connect(createMapStateToProps, mapDispatchToProps)(AddListMovieItemConnector);

View File

@@ -1,10 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import DiscoverMovieFilterModalConnector from 'DiscoverMovie/DiscoverMovieFilterModalConnector';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import FilterMenu from 'Components/Menu/FilterMenu';
import AddListMovieFilterModalConnector from 'AddMovie/AddListMovie/AddListMovieFilterModalConnector';
function DiscoverMovieFilterMenu(props) { function AddListMovieFilterMenu(props) {
const { const {
selectedFilterKey, selectedFilterKey,
filters, filters,
@@ -20,13 +20,13 @@ function DiscoverMovieFilterMenu(props) {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
filters={filters} filters={filters}
customFilters={customFilters} customFilters={customFilters}
filterModalConnectorComponent={DiscoverMovieFilterModalConnector} filterModalConnectorComponent={AddListMovieFilterModalConnector}
onFilterSelect={onFilterSelect} onFilterSelect={onFilterSelect}
/> />
); );
} }
DiscoverMovieFilterMenu.propTypes = { AddListMovieFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired, filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -34,8 +34,8 @@ DiscoverMovieFilterMenu.propTypes = {
onFilterSelect: PropTypes.func.isRequired onFilterSelect: PropTypes.func.isRequired
}; };
DiscoverMovieFilterMenu.defaultProps = { AddListMovieFilterMenu.defaultProps = {
showCustomFilters: false showCustomFilters: false
}; };
export default DiscoverMovieFilterMenu; export default AddListMovieFilterMenu;

View File

@@ -1,12 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenu from 'Components/Menu/SortMenu';
import SortMenuItem from 'Components/Menu/SortMenuItem';
import { align, sortDirections } from 'Helpers/Props'; import { align, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import SortMenu from 'Components/Menu/SortMenu';
import MenuContent from 'Components/Menu/MenuContent';
import SortMenuItem from 'Components/Menu/SortMenuItem';
function DiscoverMovieSortMenu(props) { function AddListMovieSortMenu(props) {
const { const {
sortKey, sortKey,
sortDirection, sortDirection,
@@ -26,7 +25,7 @@ function DiscoverMovieSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('Status')} Status
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -35,7 +34,7 @@ function DiscoverMovieSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('Title')} Title
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -44,7 +43,7 @@ function DiscoverMovieSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('Studio')} Studio
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -53,7 +52,7 @@ function DiscoverMovieSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('InCinemas')} In Cinemas
</SortMenuItem> </SortMenuItem>
<SortMenuItem <SortMenuItem
@@ -62,54 +61,18 @@ function DiscoverMovieSortMenu(props) {
sortDirection={sortDirection} sortDirection={sortDirection}
onPress={onSortSelect} onPress={onSortSelect}
> >
{translate('PhysicalRelease')} Physical Release
</SortMenuItem>
<SortMenuItem
name="digitalRelease"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('DigitalRelease')}
</SortMenuItem>
<SortMenuItem
name="runtime"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Runtime')}
</SortMenuItem>
<SortMenuItem
name="ratings"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Rating')}
</SortMenuItem>
<SortMenuItem
name="certification"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Certification')}
</SortMenuItem> </SortMenuItem>
</MenuContent> </MenuContent>
</SortMenu> </SortMenu>
); );
} }
DiscoverMovieSortMenu.propTypes = { AddListMovieSortMenu.propTypes = {
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired onSortSelect: PropTypes.func.isRequired
}; };
export default DiscoverMovieSortMenu; export default AddListMovieSortMenu;

View File

@@ -1,12 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenu from 'Components/Menu/ViewMenu';
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align } from 'Helpers/Props'; import { align } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import ViewMenu from 'Components/Menu/ViewMenu';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
function DiscoverMovieViewMenu(props) { function AddListMovieViewMenu(props) {
const { const {
view, view,
isDisabled, isDisabled,
@@ -24,7 +23,7 @@ function DiscoverMovieViewMenu(props) {
selectedView={view} selectedView={view}
onPress={onViewSelect} onPress={onViewSelect}
> >
{translate('Table')} Table
</ViewMenuItem> </ViewMenuItem>
<ViewMenuItem <ViewMenuItem
@@ -32,7 +31,7 @@ function DiscoverMovieViewMenu(props) {
selectedView={view} selectedView={view}
onPress={onViewSelect} onPress={onViewSelect}
> >
{translate('Posters')} Posters
</ViewMenuItem> </ViewMenuItem>
<ViewMenuItem <ViewMenuItem
@@ -40,17 +39,17 @@ function DiscoverMovieViewMenu(props) {
selectedView={view} selectedView={view}
onPress={onViewSelect} onPress={onViewSelect}
> >
{translate('Overview')} Overview
</ViewMenuItem> </ViewMenuItem>
</MenuContent> </MenuContent>
</ViewMenu> </ViewMenu>
); );
} }
DiscoverMovieViewMenu.propTypes = { AddListMovieViewMenu.propTypes = {
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
isDisabled: PropTypes.bool.isRequired, isDisabled: PropTypes.bool.isRequired,
onViewSelect: PropTypes.func.isRequired onViewSelect: PropTypes.func.isRequired
}; };
export default DiscoverMovieViewMenu; export default AddListMovieViewMenu;

View File

@@ -1,5 +1,13 @@
$hoverScale: 1.05; $hoverScale: 1.05;
.container {
&:hover {
.content {
background-color: $tableRowHoverBackgroundColor;
}
}
}
.content { .content {
display: flex; display: flex;
flex-grow: 1; flex-grow: 1;
@@ -9,13 +17,6 @@ $hoverScale: 1.05;
position: relative; position: relative;
} }
.editorSelect {
position: absolute;
top: 0;
left: 5px;
z-index: 3;
}
.posterContainer { .posterContainer {
position: relative; position: relative;
} }
@@ -32,16 +33,17 @@ $hoverScale: 1.05;
} }
} }
.alreadyExistsIcon { .ended {
margin-left: 10px; position: absolute;
color: #37bc9b; top: 0;
pointer-events: all; right: 0;
} z-index: 1;
width: 0;
.exclusionIcon { height: 0;
margin-left: 10px; border-width: 0 25px 25px 0;
color: $dangerColor; border-style: solid;
pointer-events: all; border-color: transparent $dangerColor transparent transparent;
color: $white;
} }
.info { .info {
@@ -56,15 +58,12 @@ $hoverScale: 1.05;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
flex: 0 0 auto; flex: 0 0 auto;
margin-bottom: 5px; margin-bottom: 10px;
font-weight: 300;
font-size: 30px;
line-height: 32px; line-height: 32px;
} }
.lists {
display: flex;
margin-bottom: 5px;
}
.title { .title {
@add-mixin truncate; @add-mixin truncate;
composes: link; composes: link;
@@ -72,7 +71,6 @@ $hoverScale: 1.05;
flex: 1 0 1px; flex: 1 0 1px;
font-weight: 300; font-weight: 300;
font-size: 30px; font-size: 30px;
line-height: 1;
} }
.actions { .actions {

View File

@@ -0,0 +1,156 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import MoviePoster from 'Movie/MoviePoster';
import Link from 'Components/Link/Link';
import AddNewMovieModal from 'AddMovie/AddNewMovie/AddNewMovieModal';
import styles from './AddListMovieOverview.css';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
// Hardcoded height beased on line-height of 32 + bottom margin of 10.
// Less side-effecty than using react-measure.
const titleRowHeight = 42;
function getContentHeight(rowHeight, isSmallScreen) {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}
class AddListMovieOverview extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isNewAddMovieModalOpen: false
};
}
//
// Listeners
onPress = () => {
this.setState({ isNewAddMovieModalOpen: true });
}
onAddMovieModalClose = () => {
this.setState({ isNewAddMovieModalOpen: false });
}
//
// Render
render() {
const {
tmdbId,
title,
titleSlug,
folder,
year,
overview,
images,
posterWidth,
posterHeight,
rowHeight,
isSmallScreen,
isExistingMovie
} = this.props;
const {
isNewAddMovieModalOpen
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
const overviewHeight = contentHeight - titleRowHeight;
return (
<div className={styles.container}>
<Link
className={styles.content}
{...linkProps}
>
<div className={styles.poster}>
<div className={styles.posterContainer}>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
/>
</div>
</div>
<div className={styles.info} style={{ maxHeight: contentHeight }}>
<div className={styles.titleRow}>
{title} ({year})
</div>
<div className={styles.details}>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={overview}
/>
</div>
</div>
</Link>
<AddNewMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
</div>
);
}
}
AddListMovieOverview.propTypes = {
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
folder: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
rowHeight: PropTypes.number.isRequired,
overviewOptions: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired
};
export default AddListMovieOverview;

View File

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

View File

@@ -1,13 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized'; import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import DiscoverMovieItemConnector from 'DiscoverMovie/DiscoverMovieItemConnector';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItems';
import DiscoverMovieOverviewConnector from './DiscoverMovieOverviewConnector'; import dimensions from 'Styles/Variables/dimensions';
import styles from './DiscoverMovieOverviews.css'; import Measure from 'Components/Measure';
import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector';
import AddListMovieOverviewConnector from './AddListMovieOverviewConnector';
import styles from './AddListMovieOverviews.css';
// Poster container dimensions // Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding); const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
@@ -41,7 +41,7 @@ function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth); return Math.ceil((250 / 170) * posterWidth);
} }
class DiscoverMovieOverviews extends Component { class AddListMovieOverviews extends Component {
// //
// Lifecycle // Lifecycle
@@ -81,7 +81,7 @@ class DiscoverMovieOverviews extends Component {
if (this._grid && if (this._grid &&
(prevState.width !== width || (prevState.width !== width ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId'))) { hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
} }
@@ -104,7 +104,7 @@ class DiscoverMovieOverviews extends Component {
setGridRef = (ref) => { setGridRef = (ref) => {
this._grid = ref; this._grid = ref;
}; }
calculateGrid = (width = this.state.width, isSmallScreen) => { calculateGrid = (width = this.state.width, isSmallScreen) => {
const { const {
@@ -122,7 +122,7 @@ class DiscoverMovieOverviews extends Component {
posterHeight, posterHeight,
rowHeight rowHeight
}); });
}; }
cellRenderer = ({ key, rowIndex, style }) => { cellRenderer = ({ key, rowIndex, style }) => {
const { const {
@@ -133,9 +133,7 @@ class DiscoverMovieOverviews extends Component {
shortDateFormat, shortDateFormat,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
isSmallScreen, isSmallScreen
selectedState,
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -156,9 +154,9 @@ class DiscoverMovieOverviews extends Component {
key={key} key={key}
style={style} style={style}
> >
<DiscoverMovieItemConnector <AddListMovieItemConnector
key={movie.tmdbId} key={movie.id}
component={DiscoverMovieOverviewConnector} component={AddListMovieOverviewConnector}
sortKey={sortKey} sortKey={sortKey}
posterWidth={posterWidth} posterWidth={posterWidth}
posterHeight={posterHeight} posterHeight={posterHeight}
@@ -170,19 +168,17 @@ class DiscoverMovieOverviews extends Component {
timeFormat={timeFormat} timeFormat={timeFormat}
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
movieId={movie.tmdbId} movieId={movie.tmdbId}
isSelected={selectedState[movie.tmdbId]}
onSelectedChange={onSelectedChange}
/> />
</div> </div>
); );
}; }
// //
// Listeners // Listeners
onMeasure = ({ width }) => { onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen); this.calculateGrid(width, this.props.isSmallScreen);
}; }
// //
// Render // Render
@@ -191,8 +187,7 @@ class DiscoverMovieOverviews extends Component {
const { const {
isSmallScreen, isSmallScreen,
scroller, scroller,
items, items
selectedState
} = this.props; } = this.props;
const { const {
@@ -229,7 +224,6 @@ class DiscoverMovieOverviews extends Component {
scrollTop={scrollTop} scrollTop={scrollTop}
overscanRowCount={2} overscanRowCount={2}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
selectedState={selectedState}
scrollToAlignment={'start'} scrollToAlignment={'start'}
isScrollingOptout={true} isScrollingOptout={true}
/> />
@@ -243,7 +237,7 @@ class DiscoverMovieOverviews extends Component {
} }
} }
DiscoverMovieOverviews.propTypes = { AddListMovieOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
overviewOptions: PropTypes.object.isRequired, overviewOptions: PropTypes.object.isRequired,
@@ -253,9 +247,7 @@ DiscoverMovieOverviews.propTypes = {
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired
}; };
export default DiscoverMovieOverviews; export default AddListMovieOverviews;

View File

@@ -1,12 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CollectionOverviews from './CollectionOverviews'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import AddListMovieOverviews from './AddListMovieOverviews';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.movieCollections.overviewOptions, (state) => state.addMovie.overviewOptions,
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(overviewOptions, uiSettings, dimensions) => { (overviewOptions, uiSettings, dimensions) => {
@@ -22,4 +22,4 @@ function createMapStateToProps() {
); );
} }
export default connect(createMapStateToProps)(CollectionOverviews); export default connect(createMapStateToProps)(AddListMovieOverviews);

View File

@@ -1,15 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import DiscoverMoviePosterOptionsModalContentConnector from './DiscoverMoviePosterOptionsModalContentConnector'; import AddListMovieOverviewOptionsModalContentConnector from './AddListMovieOverviewOptionsModalContentConnector';
function DiscoverMoviePosterOptionsModal({ isOpen, onModalClose, ...otherProps }) { function AddListMovieOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<DiscoverMoviePosterOptionsModalContentConnector <AddListMovieOverviewOptionsModalContentConnector
{...otherProps} {...otherProps}
onModalClose={onModalClose} onModalClose={onModalClose}
/> />
@@ -17,9 +17,9 @@ function DiscoverMoviePosterOptionsModal({ isOpen, onModalClose, ...otherProps }
); );
} }
DiscoverMoviePosterOptionsModal.propTypes = { AddListMovieOverviewOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default DiscoverMoviePosterOptionsModal; export default AddListMovieOverviewOptionsModal;

View File

@@ -0,0 +1,132 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { inputTypes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
import FormInputGroup from 'Components/Form/FormInputGroup';
import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
const posterSizeOptions = [
{ key: 'small', value: 'Small' },
{ key: 'medium', value: 'Medium' },
{ key: 'large', value: 'Large' }
];
class AddListMovieOverviewOptionsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
size: props.size,
showStudio: props.showStudio
};
}
componentDidUpdate(prevProps) {
const {
size,
showStudio
} = this.props;
const state = {};
if (size !== prevProps.size) {
state.size = size;
}
if (showStudio !== prevProps.showStudio) {
state.showStudio = showStudio;
}
if (!_.isEmpty(state)) {
this.setState(state);
}
}
//
// Listeners
onChangeOverviewOption = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onChangeOverviewOption({ [name]: value });
});
}
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
size,
showStudio
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Overview Options
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>Poster Size</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="size"
value={size}
values={posterSizeOptions}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>Show Studio</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showStudio"
value={showStudio}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
Close
</Button>
</ModalFooter>
</ModalContent>
);
}
}
AddListMovieOverviewOptionsModalContent.propTypes = {
size: PropTypes.string.isRequired,
showStudio: PropTypes.bool.isRequired,
onChangeOverviewOption: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddListMovieOverviewOptionsModalContent;

View File

@@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setListMovieOverviewOption } from 'Store/Actions/addMovieActions';
import AddListMovieOverviewOptionsModalContent from './AddListMovieOverviewOptionsModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
(addMovie) => {
return addMovie.overviewOptions;
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onChangeOverviewOption(payload) {
dispatch(setListMovieOverviewOption(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(AddListMovieOverviewOptionsModalContent);

View File

@@ -1,5 +1,9 @@
$hoverScale: 1.05; $hoverScale: 1.05;
.container {
padding: 10px;
}
.content { .content {
transition: all 200ms ease-in; transition: all 200ms ease-in;
@@ -27,11 +31,6 @@ $hoverScale: 1.05;
background-color: $defaultColor; background-color: $defaultColor;
} }
.externalLinks {
margin-right: 0.5em;
margin-left: 0.5em;
}
.overlayTitle { .overlayTitle {
position: absolute; position: absolute;
top: 0; top: 0;
@@ -55,11 +54,10 @@ $hoverScale: 1.05;
font-size: $smallFontSize; font-size: $smallFontSize;
} }
.excluded { .ended {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
z-index: 1;
width: 0; width: 0;
height: 0; height: 0;
border-width: 0 25px 25px 0; border-width: 0 25px 25px 0;
@@ -81,13 +79,6 @@ $hoverScale: 1.05;
transition: opacity 0; transition: opacity 0;
} }
.editorSelect {
position: absolute;
top: 10px;
left: 10px;
z-index: 3;
}
.action { .action {
composes: button from '~Components/Link/IconButton.css'; composes: button from '~Components/Link/IconButton.css';

View File

@@ -0,0 +1,159 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import MoviePoster from 'Movie/MoviePoster';
import AddNewMovieModal from 'AddMovie/AddNewMovie/AddNewMovieModal';
import styles from './AddListMoviePoster.css';
class AddListMoviePoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isNewAddMovieModalOpen: false
};
}
//
// Listeners
onPress = () => {
this.setState({ isNewAddMovieModalOpen: true });
}
onAddMovieModalClose = () => {
this.setState({ isNewAddMovieModalOpen: false });
}
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
}
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
}
//
// Render
render() {
const {
tmdbId,
title,
year,
overview,
folder,
status,
titleSlug,
images,
posterWidth,
posterHeight,
showTitle,
isExistingMovie
} = this.props;
const {
hasPosterError,
isNewAddMovieModalOpen
} = this.state;
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
{
status === 'ended' &&
<div
className={styles.ended}
title="Ended"
/>
}
<Link
className={styles.link}
style={elementStyle}
{...linkProps}
>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
</Link>
</div>
{
showTitle &&
<div className={styles.title}>
{title}
</div>
}
<AddNewMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
folder={folder}
images={images}
onModalClose={this.onAddMovieModalClose}
/>
</div>
);
}
}
AddListMoviePoster.propTypes = {
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
overview: PropTypes.string.isRequired,
folder: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
showTitle: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isExistingMovie: PropTypes.bool.isRequired,
isExclusionMovie: PropTypes.bool.isRequired
};
AddListMoviePoster.defaultProps = {
statistics: {
movieFileCount: 0
}
};
export default AddListMoviePoster;

View File

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

View File

@@ -0,0 +1,30 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './AddListMoviePosterInfo.css';
function AddListMoviePosterInfo(props) {
const {
studio,
sortKey
} = props;
if (sortKey === 'studio' && studio) {
return (
<div className={styles.info}>
{studio}
</div>
);
}
return null;
}
AddListMoviePosterInfo.propTypes = {
studio: PropTypes.string,
sortKey: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired
};
export default AddListMoviePosterInfo;

View File

@@ -1,3 +1,3 @@
.lists { .grid {
flex: 1 0 auto; flex: 1 0 auto;
} }

View File

@@ -1,13 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized'; import { Grid, WindowScroller } from 'react-virtualized';
import Measure from 'Components/Measure';
import DiscoverMovieItemConnector from 'DiscoverMovie/DiscoverMovieItemConnector';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter'; import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItems';
import DiscoverMoviePosterConnector from './DiscoverMoviePosterConnector'; import dimensions from 'Styles/Variables/dimensions';
import styles from './DiscoverMoviePosters.css'; import Measure from 'Components/Measure';
import AddListMovieItemConnector from 'AddMovie/AddListMovie/AddListMovieItemConnector';
import AddListMoviePosterConnector from './AddListMoviePosterConnector';
import styles from './AddListMoviePosters.css';
// Poster container dimensions // Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding); const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
@@ -36,7 +36,9 @@ function calculateColumnWidth(width, posterSize, isSmallScreen) {
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) { function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions) {
const { const {
detailedProgressBar, detailedProgressBar,
showTitle showTitle,
showMonitored,
showQualityProfile
} = posterOptions; } = posterOptions;
const nextAiringHeight = 19; const nextAiringHeight = 19;
@@ -52,6 +54,14 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
heights.push(19); heights.push(19);
} }
if (showMonitored) {
heights.push(19);
}
if (showQualityProfile) {
heights.push(19);
}
switch (sortKey) { switch (sortKey) {
case 'studio': case 'studio':
default: default:
@@ -65,7 +75,7 @@ function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth); return Math.ceil((250 / 170) * posterWidth);
} }
class DiscoverMoviePosters extends Component { class AddListMoviePosters extends Component {
// //
// Lifecycle // Lifecycle
@@ -84,7 +94,6 @@ class DiscoverMoviePosters extends Component {
this._isInitialized = false; this._isInitialized = false;
this._grid = null; this._grid = null;
this._padding = props.isSmallScreen ? columnPaddingSmallScreen : columnPadding;
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@@ -92,9 +101,7 @@ class DiscoverMoviePosters extends Component {
items, items,
sortKey, sortKey,
posterOptions, posterOptions,
jumpToCharacter, jumpToCharacter
isSmallScreen,
selectedState
} = this.props; } = this.props;
const { const {
@@ -106,7 +113,7 @@ class DiscoverMoviePosters extends Component {
if (prevProps.sortKey !== sortKey || if (prevProps.sortKey !== sortKey ||
prevProps.posterOptions !== posterOptions) { prevProps.posterOptions !== posterOptions) {
this.calculateGrid(width, isSmallScreen); this.calculateGrid();
} }
if (this._grid && if (this._grid &&
@@ -114,8 +121,7 @@ class DiscoverMoviePosters extends Component {
prevState.columnWidth !== columnWidth || prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount || prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
prevProps.selectedState !== selectedState || hasDifferentItemsOrOrder(prevProps.items, items))) {
hasDifferentItemsOrOrder(prevProps.items, items, 'tmdbId'))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
} }
@@ -139,7 +145,7 @@ class DiscoverMoviePosters extends Component {
setGridRef = (ref) => { setGridRef = (ref) => {
this._grid = ref; this._grid = ref;
}; }
calculateGrid = (width = this.state.width, isSmallScreen) => { calculateGrid = (width = this.state.width, isSmallScreen) => {
const { const {
@@ -147,9 +153,10 @@ class DiscoverMoviePosters extends Component {
posterOptions posterOptions
} = this.props; } = this.props;
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen); const columnWidth = calculateColumnWidth(width, posterOptions.size, isSmallScreen);
const columnCount = Math.max(Math.floor(width / columnWidth), 1); const columnCount = Math.max(Math.floor(width / columnWidth), 1);
const posterWidth = columnWidth - this._padding * 2; const posterWidth = columnWidth - padding;
const posterHeight = calculatePosterHeight(posterWidth); const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions); const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions);
@@ -161,7 +168,7 @@ class DiscoverMoviePosters extends Component {
posterHeight, posterHeight,
rowHeight rowHeight
}); });
}; }
cellRenderer = ({ key, rowIndex, columnIndex, style }) => { cellRenderer = ({ key, rowIndex, columnIndex, style }) => {
const { const {
@@ -170,9 +177,7 @@ class DiscoverMoviePosters extends Component {
posterOptions, posterOptions,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat, timeFormat
selectedState,
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -185,8 +190,7 @@ class DiscoverMoviePosters extends Component {
showTitle showTitle
} = posterOptions; } = posterOptions;
const movieIdx = rowIndex * columnCount + columnIndex; const movie = items[rowIndex * columnCount + columnIndex];
const movie = items[movieIdx];
if (!movie) { if (!movie) {
return null; return null;
@@ -194,16 +198,12 @@ class DiscoverMoviePosters extends Component {
return ( return (
<div <div
className={styles.container}
key={key} key={key}
style={{ style={style}
...style,
padding: this._padding
}}
> >
<DiscoverMovieItemConnector <AddListMovieItemConnector
key={movie.tmdbId} key={movie.id}
component={DiscoverMoviePosterConnector} component={AddListMoviePosterConnector}
sortKey={sortKey} sortKey={sortKey}
posterWidth={posterWidth} posterWidth={posterWidth}
posterHeight={posterHeight} posterHeight={posterHeight}
@@ -212,19 +212,17 @@ class DiscoverMoviePosters extends Component {
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
movieId={movie.tmdbId} movieId={movie.tmdbId}
isSelected={selectedState[movie.tmdbId]}
onSelectedChange={onSelectedChange}
/> />
</div> </div>
); );
}; }
// //
// Listeners // Listeners
onMeasure = ({ width }) => { onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen); this.calculateGrid(width, this.props.isSmallScreen);
}; }
// //
// Render // Render
@@ -233,8 +231,7 @@ class DiscoverMoviePosters extends Component {
const { const {
isSmallScreen, isSmallScreen,
scroller, scroller,
items, items
selectedState
} = this.props; } = this.props;
const { const {
@@ -275,7 +272,6 @@ class DiscoverMoviePosters extends Component {
scrollTop={scrollTop} scrollTop={scrollTop}
overscanRowCount={2} overscanRowCount={2}
cellRenderer={this.cellRenderer} cellRenderer={this.cellRenderer}
selectedState={selectedState}
scrollToAlignment={'start'} scrollToAlignment={'start'}
isScrollingOptOut={true} isScrollingOptOut={true}
/> />
@@ -289,7 +285,7 @@ class DiscoverMoviePosters extends Component {
} }
} }
DiscoverMoviePosters.propTypes = { AddListMoviePosters.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string, sortKey: PropTypes.string,
posterOptions: PropTypes.object.isRequired, posterOptions: PropTypes.object.isRequired,
@@ -298,9 +294,7 @@ DiscoverMoviePosters.propTypes = {
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired
}; };
export default DiscoverMoviePosters; export default AddListMoviePosters;

View File

@@ -1,12 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import DiscoverMoviePosters from './DiscoverMoviePosters'; import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import AddListMoviePosters from './AddListMoviePosters';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.discoverMovie.posterOptions, (state) => state.addMovie.posterOptions,
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(posterOptions, uiSettings, dimensions) => { (posterOptions, uiSettings, dimensions) => {
@@ -21,4 +21,4 @@ function createMapStateToProps() {
); );
} }
export default connect(createMapStateToProps)(DiscoverMoviePosters); export default connect(createMapStateToProps)(AddListMoviePosters);

View File

@@ -1,15 +1,15 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import CollectionOverviewOptionsModalContentConnector from './CollectionOverviewOptionsModalContentConnector'; import AddListMoviePosterOptionsModalContentConnector from './AddListMoviePosterOptionsModalContentConnector';
function CollectionOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) { function AddListMoviePosterOptionsModal({ isOpen, onModalClose, ...otherProps }) {
return ( return (
<Modal <Modal
isOpen={isOpen} isOpen={isOpen}
onModalClose={onModalClose} onModalClose={onModalClose}
> >
<CollectionOverviewOptionsModalContentConnector <AddListMoviePosterOptionsModalContentConnector
{...otherProps} {...otherProps}
onModalClose={onModalClose} onModalClose={onModalClose}
/> />
@@ -17,9 +17,9 @@ function CollectionOverviewOptionsModal({ isOpen, onModalClose, ...otherProps })
); );
} }
CollectionOverviewOptionsModal.propTypes = { AddListMoviePosterOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default CollectionOverviewOptionsModal; export default AddListMoviePosterOptionsModal;

View File

@@ -1,25 +1,24 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { inputTypes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button'; import FormInputGroup from 'Components/Form/FormInputGroup';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import ModalBody from 'Components/Modal/ModalBody';
import translate from 'Utilities/String/translate'; import ModalFooter from 'Components/Modal/ModalFooter';
const posterSizeOptions = [ const posterSizeOptions = [
{ key: 'small', value: translate('Small') }, { key: 'small', value: 'Small' },
{ key: 'medium', value: translate('Medium') }, { key: 'medium', value: 'Medium' },
{ key: 'large', value: translate('Large') } { key: 'large', value: 'Large' }
]; ];
class DiscoverMoviePosterOptionsModalContent extends Component { class AddListMoviePosterOptionsModalContent extends Component {
// //
// Lifecycle // Lifecycle
@@ -29,16 +28,14 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
this.state = { this.state = {
size: props.size, size: props.size,
showTitle: props.showTitle, showTitle: props.showTitle
includeRecommendations: props.includeRecommendations
}; };
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
const { const {
size, size,
showTitle, showTitle
includeRecommendations
} = this.props; } = this.props;
const state = {}; const state = {};
@@ -51,10 +48,6 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
state.showTitle = showTitle; state.showTitle = showTitle;
} }
if (includeRecommendations !== prevProps.includeRecommendations) {
state.includeRecommendations = includeRecommendations;
}
if (!_.isEmpty(state)) { if (!_.isEmpty(state)) {
this.setState(state); this.setState(state);
} }
@@ -69,17 +62,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
}, () => { }, () => {
this.props.onChangePosterOption({ [name]: value }); this.props.onChangePosterOption({ [name]: value });
}); });
}; }
onChangeOption = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onChangeOption({
[name]: value
});
});
};
// //
// Render // Render
@@ -91,8 +74,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
const { const {
size, size,
showTitle, showTitle
includeRecommendations
} = this.state; } = this.state;
return ( return (
@@ -104,19 +86,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
<ModalBody> <ModalBody>
<Form> <Form>
<FormGroup> <FormGroup>
<FormLabel>{translate('IncludeRadarrRecommendations')}</FormLabel> <FormLabel>Poster Size</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeRecommendations"
value={includeRecommendations}
helpText={translate('IncludeRecommendationsHelpText')}
onChange={this.onChangeOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('PosterSize')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
@@ -128,13 +98,13 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('ShowTitle')}</FormLabel> <FormLabel>Show Title</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="showTitle" name="showTitle"
value={showTitle} value={showTitle}
helpText={translate('ShowTitleHelpText')} helpText="Show movie title under poster"
onChange={this.onChangePosterOption} onChange={this.onChangePosterOption}
/> />
</FormGroup> </FormGroup>
@@ -145,7 +115,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
{translate('Close')} Close
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
@@ -153,13 +123,11 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
} }
} }
DiscoverMoviePosterOptionsModalContent.propTypes = { AddListMoviePosterOptionsModalContent.propTypes = {
size: PropTypes.string.isRequired, size: PropTypes.string.isRequired,
showTitle: PropTypes.bool.isRequired, showTitle: PropTypes.bool.isRequired,
includeRecommendations: PropTypes.bool.isRequired,
onChangePosterOption: PropTypes.func.isRequired, onChangePosterOption: PropTypes.func.isRequired,
onChangeOption: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default DiscoverMoviePosterOptionsModalContent; export default AddListMoviePosterOptionsModalContent;

View File

@@ -0,0 +1,23 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setListMoviePosterOption } from 'Store/Actions/addMovieActions';
import AddListMoviePosterOptionsModalContent from './AddListMoviePosterOptionsModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.addMovie,
(addMovie) => {
return addMovie.posterOptions;
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onChangePosterOption(payload) {
dispatch(setListMoviePosterOption(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(AddListMoviePosterOptionsModalContent);

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
class DiscoverMovieActionsCell extends Component { class AddListMovieActionsCell extends Component {
// //
// Render // Render
@@ -47,8 +47,8 @@ class DiscoverMovieActionsCell extends Component {
} }
} }
DiscoverMovieActionsCell.propTypes = { AddListMovieActionsCell.propTypes = {
id: PropTypes.number.isRequired id: PropTypes.number.isRequired
}; };
export default DiscoverMovieActionsCell; export default AddListMovieActionsCell;

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