Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
748e846c21 | ||
|
|
6b52d11577 | ||
|
|
62a0454bf7 | ||
|
|
deddb0ac6c | ||
|
|
923b959b6c | ||
|
|
b088a2220a | ||
|
|
e29e962bd5 | ||
|
|
62173c5258 | ||
|
|
e3bb4d9b59 | ||
|
|
e973701f1c | ||
|
|
62efca5084 | ||
|
|
f638278f4e | ||
|
|
69811b1291 | ||
|
|
802e5ac151 | ||
|
|
09fe1bfc96 | ||
|
|
1dd0df58ca | ||
|
|
747f4c87d3 | ||
|
|
ac2b9516e1 | ||
|
|
1f72e1de74 | ||
|
|
6b743439eb | ||
|
|
b254ee548d | ||
|
|
d8e0fa6ac2 | ||
|
|
bdb4f490b9 | ||
|
|
588771f3ee | ||
|
|
f0ef6c3601 | ||
|
|
01b0365884 | ||
|
|
e715905ea1 | ||
|
|
5d4ec1272d | ||
|
|
2d40914482 | ||
|
|
420e5fd730 | ||
|
|
b95c2e5c8b | ||
|
|
7ddc8d416c | ||
|
|
6160bbf113 | ||
|
|
a2f666445f | ||
|
|
92d6e81236 | ||
|
|
227101d59e | ||
|
|
080d0a9b04 | ||
|
|
1b58ae7d47 | ||
|
|
a5e2e5777c | ||
|
|
ddc2b42923 | ||
|
|
3ac3737de9 | ||
|
|
01ad015b14 | ||
|
|
3d57d5aba7 | ||
|
|
d65fe3a530 | ||
|
|
e100759e71 | ||
|
|
df068e9f0a | ||
|
|
c27f08738a | ||
|
|
61629a527c | ||
|
|
5291f42905 | ||
|
|
0ce5857094 | ||
|
|
443078a7e4 | ||
|
|
dbdda0da13 | ||
|
|
b5e1b83de3 | ||
|
|
8e43f5c4ae | ||
|
|
e6d3954e79 |
254
.editorconfig
@@ -2,264 +2,14 @@
|
||||
# editorconfig.org
|
||||
root = true
|
||||
|
||||
# NOTE: Requires **VS2019 16.3** or later
|
||||
|
||||
# Stylecop.ruleset
|
||||
# Description: Rules for Radarr
|
||||
|
||||
# Code files
|
||||
[*.cs]
|
||||
[*.{cs,html,js,hbs}]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# Sort using and Import directives with System.* appearing first
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
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.required_prefix = _
|
||||
|
||||
# Prefer "var" everywhere
|
||||
csharp_style_var_for_built_in_types = true:suggestion
|
||||
csharp_style_var_when_type_is_apparent = 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.SX1101.severity = warning
|
||||
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.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.CA1401.severity = suggestion
|
||||
dotnet_diagnostic.CA1507.severity = suggestion
|
||||
dotnet_diagnostic.CA1707.severity = suggestion
|
||||
dotnet_diagnostic.CA1710.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.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.CA1814.severity = suggestion
|
||||
dotnet_diagnostic.CA1815.severity = suggestion
|
||||
dotnet_diagnostic.CA1816.severity = suggestion
|
||||
dotnet_diagnostic.CA1819.severity = suggestion
|
||||
dotnet_diagnostic.CA1820.severity = suggestion
|
||||
dotnet_diagnostic.CA1821.severity = suggestion
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
||||
dotnet_diagnostic.CA1823.severity = suggestion
|
||||
dotnet_diagnostic.CA1824.severity = suggestion
|
||||
dotnet_diagnostic.CA1825.severity = suggestion
|
||||
dotnet_diagnostic.CA1826.severity = suggestion
|
||||
dotnet_diagnostic.CA1827.severity = suggestion
|
||||
dotnet_diagnostic.CA1828.severity = suggestion
|
||||
dotnet_diagnostic.CA1829.severity = suggestion
|
||||
dotnet_diagnostic.CA1834.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.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.CA5397.severity = suggestion
|
||||
|
||||
|
||||
|
||||
[*.{js,html,js,hbs,less,css}]
|
||||
[*.less]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"paths": [
|
||||
"frontend/src/**/*.js"
|
||||
],
|
||||
"ignored": [
|
||||
"**/node_modules/**/*"
|
||||
],
|
||||
"port": 5004
|
||||
}
|
||||
18
.gitattributes
vendored
@@ -7,4 +7,20 @@ macOS/Radarr text eol=lf
|
||||
|
||||
# Custom for Visual Studio
|
||||
*.cs diff=csharp
|
||||
*.sln merge=union
|
||||
#*.sln merge=union
|
||||
#*.csproj merge=union
|
||||
#*.vbproj merge=union
|
||||
#*.fsproj merge=union
|
||||
#*.dbproj merge=union
|
||||
|
||||
# Standard to msysgit
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
305
.gitchangelog.rc
@@ -1,305 +0,0 @@
|
||||
# -*- coding: utf-8; mode: python -*-
|
||||
##
|
||||
## Format
|
||||
##
|
||||
## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
|
||||
##
|
||||
## Description
|
||||
##
|
||||
## ACTION is one of 'chg', 'fix', 'new'
|
||||
##
|
||||
## Is WHAT the change is about.
|
||||
##
|
||||
## 'chg' is for refactor, small improvement, cosmetic changes...
|
||||
## 'fix' is for bug fixes
|
||||
## 'new' is for new features, big improvement
|
||||
##
|
||||
## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
|
||||
##
|
||||
## Is WHO is concerned by the change.
|
||||
##
|
||||
## 'dev' is for developpers (API changes, refactors...)
|
||||
## 'usr' is for final users (UI changes)
|
||||
## 'pkg' is for packagers (packaging changes)
|
||||
## 'test' is for testers (test only related changes)
|
||||
## 'doc' is for doc guys (doc only changes)
|
||||
##
|
||||
## COMMIT_MSG is ... well ... the commit message itself.
|
||||
##
|
||||
## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
|
||||
##
|
||||
## They are preceded with a '!' or a '@' (prefer the former, as the
|
||||
## latter is wrongly interpreted in github.) Commonly used tags are:
|
||||
##
|
||||
## 'refactor' is obviously for refactoring code only
|
||||
## 'minor' is for a very meaningless change (a typo, adding a comment)
|
||||
## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
|
||||
## 'wip' is for partial functionality but complete subfunctionality.
|
||||
##
|
||||
## Example:
|
||||
##
|
||||
## new: usr: support of bazaar implemented
|
||||
## chg: re-indentend some lines !cosmetic
|
||||
## new: dev: updated code to be compatible with last version of killer lib.
|
||||
## fix: pkg: updated year of licence coverage.
|
||||
## new: test: added a bunch of test around user usability of feature X.
|
||||
## fix: typo in spelling my name in comment. !minor
|
||||
##
|
||||
## Please note that multi-line commit message are supported, and only the
|
||||
## first line will be considered as the "summary" of the commit message. So
|
||||
## tags, and other rules only applies to the summary. The body of the commit
|
||||
## message will be displayed in the changelog without reformatting.
|
||||
|
||||
|
||||
##
|
||||
## ``ignore_regexps`` is a line of regexps
|
||||
##
|
||||
## Any commit having its full commit message matching any regexp listed here
|
||||
## will be ignored and won't be reported in the changelog.
|
||||
##
|
||||
ignore_regexps = [
|
||||
r'@minor', r'!minor',
|
||||
r'@cosmetic', r'!cosmetic',
|
||||
r'@refactor', r'!refactor',
|
||||
r'@wip', r'!wip',
|
||||
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
|
||||
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
|
||||
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
|
||||
r'^$', ## ignore commits with empty messages
|
||||
]
|
||||
|
||||
|
||||
## ``section_regexps`` is a list of 2-tuples associating a string label and a
|
||||
## list of regexp
|
||||
##
|
||||
## Commit messages will be classified in sections thanks to this. Section
|
||||
## titles are the label, and a commit is classified under this section if any
|
||||
## of the regexps associated is matching.
|
||||
##
|
||||
## Please note that ``section_regexps`` will only classify commits and won't
|
||||
## make any changes to the contents. So you'll probably want to go check
|
||||
## ``subject_process`` (or ``body_process``) to do some changes to the subject,
|
||||
## whenever you are tweaking this variable.
|
||||
##
|
||||
section_regexps = [
|
||||
('**New features**', [
|
||||
r'^[aA]dded?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?(.*)$',
|
||||
r'^[uU]pdated?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
r'^[cC]hanged?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
r'^[nN]ew?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
('**Fixes**', [
|
||||
r'^(?![mM]erge\s*)'
|
||||
]
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
|
||||
## ``body_process`` is a callable
|
||||
##
|
||||
## This callable will be given the original body and result will
|
||||
## be used in the changelog.
|
||||
##
|
||||
## Available constructs are:
|
||||
##
|
||||
## - any python callable that take one txt argument and return txt argument.
|
||||
##
|
||||
## - ReSub(pattern, replacement): will apply regexp substitution.
|
||||
##
|
||||
## - Indent(chars=" "): will indent the text with the prefix
|
||||
## Please remember that template engines gets also to modify the text and
|
||||
## will usually indent themselves the text if needed.
|
||||
##
|
||||
## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
|
||||
##
|
||||
## - noop: do nothing
|
||||
##
|
||||
## - ucfirst: ensure the first letter is uppercase.
|
||||
## (usually used in the ``subject_process`` pipeline)
|
||||
##
|
||||
## - final_dot: ensure text finishes with a dot
|
||||
## (usually used in the ``subject_process`` pipeline)
|
||||
##
|
||||
## - strip: remove any spaces before or after the content of the string
|
||||
##
|
||||
## - SetIfEmpty(msg="No commit message."): will set the text to
|
||||
## whatever given ``msg`` if the current text is empty.
|
||||
##
|
||||
## Additionally, you can `pipe` the provided filters, for instance:
|
||||
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
|
||||
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
|
||||
#body_process = noop
|
||||
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
|
||||
|
||||
|
||||
## ``subject_process`` is a callable
|
||||
##
|
||||
## This callable will be given the original subject and result will
|
||||
## be used in the changelog.
|
||||
## subject_process = (strip |
|
||||
## ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r' \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'[#\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'[#\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..."]
|
||||
@@ -1,311 +0,0 @@
|
||||
# -*- coding: utf-8; mode: python -*-
|
||||
##
|
||||
## Format
|
||||
##
|
||||
## ACTION: [AUDIENCE:] COMMIT_MSG [!TAG ...]
|
||||
##
|
||||
## Description
|
||||
##
|
||||
## ACTION is one of 'chg', 'fix', 'new'
|
||||
##
|
||||
## Is WHAT the change is about.
|
||||
##
|
||||
## 'chg' is for refactor, small improvement, cosmetic changes...
|
||||
## 'fix' is for bug fixes
|
||||
## 'new' is for new features, big improvement
|
||||
##
|
||||
## AUDIENCE is optional and one of 'dev', 'usr', 'pkg', 'test', 'doc'
|
||||
##
|
||||
## Is WHO is concerned by the change.
|
||||
##
|
||||
## 'dev' is for developpers (API changes, refactors...)
|
||||
## 'usr' is for final users (UI changes)
|
||||
## 'pkg' is for packagers (packaging changes)
|
||||
## 'test' is for testers (test only related changes)
|
||||
## 'doc' is for doc guys (doc only changes)
|
||||
##
|
||||
## COMMIT_MSG is ... well ... the commit message itself.
|
||||
##
|
||||
## TAGs are additionnal adjective as 'refactor' 'minor' 'cosmetic'
|
||||
##
|
||||
## They are preceded with a '!' or a '@' (prefer the former, as the
|
||||
## latter is wrongly interpreted in github.) Commonly used tags are:
|
||||
##
|
||||
## 'refactor' is obviously for refactoring code only
|
||||
## 'minor' is for a very meaningless change (a typo, adding a comment)
|
||||
## 'cosmetic' is for cosmetic driven change (re-indentation, 80-col...)
|
||||
## 'wip' is for partial functionality but complete subfunctionality.
|
||||
##
|
||||
## Example:
|
||||
##
|
||||
## new: usr: support of bazaar implemented
|
||||
## chg: re-indentend some lines !cosmetic
|
||||
## new: dev: updated code to be compatible with last version of killer lib.
|
||||
## fix: pkg: updated year of licence coverage.
|
||||
## new: test: added a bunch of test around user usability of feature X.
|
||||
## fix: typo in spelling my name in comment. !minor
|
||||
##
|
||||
## Please note that multi-line commit message are supported, and only the
|
||||
## first line will be considered as the "summary" of the commit message. So
|
||||
## tags, and other rules only applies to the summary. The body of the commit
|
||||
## message will be displayed in the changelog without reformatting.
|
||||
|
||||
|
||||
##
|
||||
## ``ignore_regexps`` is a line of regexps
|
||||
##
|
||||
## Any commit having its full commit message matching any regexp listed here
|
||||
## will be ignored and won't be reported in the changelog.
|
||||
##
|
||||
ignore_regexps = [
|
||||
r'@minor', r'!minor',
|
||||
r'@cosmetic', r'!cosmetic',
|
||||
r'@refactor', r'!refactor',
|
||||
r'@wip', r'!wip',
|
||||
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[p|P]kg:',
|
||||
r'^([cC]hg|[fF]ix|[nN]ew)\s*:\s*[d|D]ev:',
|
||||
r'^(.{3,3}\s*:)?\s*[fF]irst commit.?\s*$',
|
||||
r'^$', ## ignore commits with empty messages
|
||||
]
|
||||
|
||||
|
||||
## ``section_regexps`` is a list of 2-tuples associating a string label and a
|
||||
## list of regexp
|
||||
##
|
||||
## Commit messages will be classified in sections thanks to this. Section
|
||||
## titles are the label, and a commit is classified under this section if any
|
||||
## of the regexps associated is matching.
|
||||
##
|
||||
## Please note that ``section_regexps`` will only classify commits and won't
|
||||
## make any changes to the contents. So you'll probably want to go check
|
||||
## ``subject_process`` (or ``body_process``) to do some changes to the subject,
|
||||
## whenever you are tweaking this variable.
|
||||
##
|
||||
section_regexps = [
|
||||
('**New features:**', [
|
||||
r'^[aA]dded?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
r'^[uU]pdated?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
r'^[cC]hanged?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
r'^[nN]ew?\s*:?\s*((dev|use?r|pkg|test|doc)\s*:\s*)?([^\n]*)$',
|
||||
]),
|
||||
('**Fixes:**', [
|
||||
r'^(?![mM]erge\s*)'
|
||||
]
|
||||
),
|
||||
|
||||
]
|
||||
|
||||
|
||||
## ``body_process`` is a callable
|
||||
##
|
||||
## This callable will be given the original body and result will
|
||||
## be used in the changelog.
|
||||
##
|
||||
## Available constructs are:
|
||||
##
|
||||
## - any python callable that take one txt argument and return txt argument.
|
||||
##
|
||||
## - ReSub(pattern, replacement): will apply regexp substitution.
|
||||
##
|
||||
## - Indent(chars=" "): will indent the text with the prefix
|
||||
## Please remember that template engines gets also to modify the text and
|
||||
## will usually indent themselves the text if needed.
|
||||
##
|
||||
## - Wrap(regexp=r"\n\n"): re-wrap text in separate paragraph to fill 80-Columns
|
||||
##
|
||||
## - noop: do nothing
|
||||
##
|
||||
## - ucfirst: ensure the first letter is uppercase.
|
||||
## (usually used in the ``subject_process`` pipeline)
|
||||
##
|
||||
## - final_dot: ensure text finishes with a dot
|
||||
## (usually used in the ``subject_process`` pipeline)
|
||||
##
|
||||
## - strip: remove any spaces before or after the content of the string
|
||||
##
|
||||
## - SetIfEmpty(msg="No commit message."): will set the text to
|
||||
## whatever given ``msg`` if the current text is empty.
|
||||
##
|
||||
## Additionally, you can `pipe` the provided filters, for instance:
|
||||
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)') | Indent(chars=" ")
|
||||
#body_process = Wrap(regexp=r'\n(?=\w+\s*:)')
|
||||
#body_process = noop
|
||||
body_process = ReSub(r'((^|\n)[A-Z]\w+(-\w+)*: .*(\n\s+.*)*)+$', r'') | strip
|
||||
|
||||
|
||||
## ``subject_process`` is a callable
|
||||
##
|
||||
## This callable will be given the original subject and result will
|
||||
## be used in the changelog.
|
||||
## subject_process = (strip |
|
||||
## ReSub(r'^([aA]dd(ed?)?|[nN]ew)(\s?:?\s)(.*)$', r' \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'[#\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+"..."]
|
||||
7
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,9 +2,6 @@
|
||||
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: ''
|
||||
|
||||
---
|
||||
|
||||
@@ -24,11 +21,11 @@ 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):**
|
||||
**Desktop (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]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**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!**
|
||||
|
||||
8
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support via Discord
|
||||
url: https://discord.gg/AD3UP37
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
- name: Support via Reddit
|
||||
url: https://reddit.com/r/radarr
|
||||
about: Discuss and search thru support topics.
|
||||
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,9 +1,6 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: feature request
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -6,7 +6,6 @@ YES | NO
|
||||
|
||||
#### Todos
|
||||
- [ ] Tests
|
||||
- [ ] Translation Keys
|
||||
|
||||
#### Issues Fixed or Closed by this PR
|
||||
|
||||
|
||||
2
.github/stale.yml
vendored
@@ -7,7 +7,6 @@ exemptLabels:
|
||||
- feature request
|
||||
- parser
|
||||
- confirmed
|
||||
- aphrodite
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
@@ -15,4 +14,3 @@ markComment: >
|
||||
This issue has been automatically marked as stale because it has not had recent activity. Please verify that this is still an issue with the latest version of Radarr and report back. Otherwise this issue will be closed.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
only: issues
|
||||
|
||||
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "src/ExternalModules/CurlSharp"]
|
||||
path = src/ExternalModules/CurlSharp
|
||||
url = https://github.com/Sonarr/CurlSharp.git
|
||||
branch = master
|
||||
1096
CHANGELOG.md
@@ -7,7 +7,21 @@ Setup guides, FAQ, the more information we have on the wiki the better.
|
||||
|
||||
## Development ##
|
||||
|
||||
See the readme for information on setting up your development environment.
|
||||
### Tools required ###
|
||||
- Visual Studio 2015
|
||||
- HTML/Javascript editor of choice (Sublime Text/Webstorm/Atom/etc)
|
||||
- npm (node package manager)
|
||||
- git
|
||||
|
||||
### Getting started ###
|
||||
|
||||
1. Fork Radarr
|
||||
2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)*
|
||||
3. Run `npm install`
|
||||
4. Run `npm start` - Used to compile the UI components and copy them.
|
||||
Leave this window open.
|
||||
If you have gulp globally installed you can use `gulp watch` instead
|
||||
5. Compile in Visual Studio
|
||||
|
||||
### Contributing Code ###
|
||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/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)
|
||||
|
||||
BIN
Logo/1024.png
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 44 KiB |
BIN
Logo/128.png
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 5.0 KiB |
BIN
Logo/16.png
|
Before Width: | Height: | Size: 577 B After Width: | Height: | Size: 605 B |
BIN
Logo/256.png
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 28 KiB |
BIN
Logo/32.png
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.5 KiB |
BIN
Logo/400.png
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
BIN
Logo/48.png
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.0 KiB |
BIN
Logo/512.png
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
BIN
Logo/64.png
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
Logo/800.png
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 31 KiB |
BIN
Logo/text256.png
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 28 KiB |
71
README.md
@@ -1,43 +1,43 @@
|
||||
# Radarr
|
||||
|
||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||
[](https://github.com/Radarr/Radarr/wiki/Docker)
|
||||

|
||||
[](#backers) [](#sponsors)
|
||||
<p align="center">
|
||||
<img src="/Logo/text256.png" alt="Radarr">
|
||||
</p>
|
||||
|
||||
Radarr is an __independent__ fork of [Sonarr](https://github.com/Sonarr/Sonarr) reworked for automatically downloading movies via Usenet and BitTorrent.
|
||||
|
||||
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
|
||||
|
||||
[](https://github.com/Radarr/Radarr/wiki/Installation)
|
||||
[](https://github.com/Radarr/Radarr/wiki/Docker)
|
||||
[](https://github.com/Radarr/Radarr/wiki/Docker)
|
||||
[](https://github.com/Radarr/Radarr/wiki/Setup-Guide)
|
||||
[](https://github.com/Radarr/Radarr/wiki/FAQ)
|
||||
|
||||
|
||||
If you are using Docker please ensure your Docker paths are setup correctly using [this guide to facilitate](https://old.reddit.com/r/usenet/wiki/docker) hardlinks and minimize permissions issues.
|
||||
|
||||
* [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
|
||||
Please note that v0.2 will only have critical bugs resolved as of August 2020. Any additional development or features will be soley in V3.
|
||||
|
||||
| Release Type | Branch: develop (stable) | Branch: nightly (semi-unstable) | Branch: aphrodite (very-unstable) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Binary Releases | [](https://github.com/Radarr/Radarr/releases) | [](https://ci.appveyor.com/project/galli-leo/radarr-usby1/branch/develop/artifacts) | |
|
||||
| Docker - lsio | [](https://hub.docker.com/r/linuxserver/radarr) | [](https://hub.docker.com/r/linuxserver/radarr) | [](https://hub.docker.com/r/linuxserver/radarr) |
|
||||
| Docker - hotio | [](https://hub.docker.com/r/hotio/radarr) | [](https://hub.docker.com/r/hotio/radarr) | [](https://hub.docker.com/r/hotio/radarr) |
|
||||
Branch | develop (stable) | nightly (semi-unstable) |
|
||||
---|---|---
|
||||
Binary Releases | [](https://github.com/Radarr/Radarr/releases) | [](https://ci.appveyor.com/project/galli-leo/radarr-usby1/branch/develop/artifacts)
|
||||
Docker (linuxserver.io, x86_64, arm64, armhf) | [](https://store.docker.com/community/images/linuxserver/radarr) | [](https://store.docker.com/community/images/linuxserver/radarr)
|
||||
Docker (hotio, see [here](https://github.com/hotio/docker-radarr) for more information) | [](https://hub.docker.com/r/hotio/radarr) | [](https://hub.docker.com/r/hotio/radarr)
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
[](#backers)
|
||||
[](#flexible-sponsors)
|
||||
[](#sponsors)
|
||||
|
||||
[](https://discord.gg/AD3UP37)
|
||||
[](https://www.reddit.com/r/radarr)
|
||||
[](http://feathub.com/Radarr/Radarr)
|
||||
[](https://github.com/Radarr/Radarr/issues)
|
||||
[](https://github.com/Radarr/Radarr/wiki)
|
||||
|
||||
@@ -46,13 +46,23 @@ Please note that v0.2 will only have critical bugs resolved as of August 2020. A
|
||||
[](https://github.com/Radarr/Radarr/issues)
|
||||
[](https://github.com/Radarr/Radarr/pulls)
|
||||
[](http://www.gnu.org/licenses/gpl.html)
|
||||
[](https://github.com/Radarr/Radarr)
|
||||
[](https://github.com/Radarr/Radarr)
|
||||
[](https://github.com/Radarr/Radarr/releases/)
|
||||
[](https://hub.docker.com/r/linuxserver/radarr/)
|
||||
[](/CHANGELOG.md#unreleased)
|
||||
|
||||
| Service | Master | Develop |
|
||||
|----------|:---------------------------:|:----------------------------:|
|
||||
| AppVeyor | [](https://ci.appveyor.com/project/galli-leo/Radarr) | [](https://ci.appveyor.com/project/galli-leo/Radarr-usby1) |
|
||||
| Travis | [](https://travis-ci.org/Radarr/Radarr) | [](https://travis-ci.org/Radarr/Radarr) |
|
||||
|
||||
### [Site and API Status](https://status.radarr.video)
|
||||
|
||||
| API | Updates | Sites |
|
||||
|-------|:----:|:----:|
|
||||
| [](https://api.radarr.video/v2/) | [](https://radarr.aeonlucid.com) | [](https://mappings.radarr.video/)
|
||||
| [](https://staging.api.radarr.video/) | [](https://api.github.com/v3/) | [](https://radarr.video/)
|
||||
|
||||
Radarr is currently undergoing rapid development and pull requests are actively added into the repository.
|
||||
|
||||
## Features
|
||||
@@ -75,30 +85,34 @@ Radarr is currently undergoing rapid development and pull requests are actively
|
||||
* 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 or Trakt (v3) (A complete list can be found [here](https://github.com/Radarr/Radarr/issues/114))
|
||||
* 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 new beautiful UI (v3)
|
||||
* And a beautiful UI
|
||||
* Importing Metadata such as trailers or subtitles
|
||||
* 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
|
||||
|
||||
#### [Feature Requests](https://github.com/Radarr/Radarr/issues/new?assignees=&labels=feature+request&template=feature_request.md&title=)
|
||||
### Planned Features
|
||||
|
||||
See the [Roadmap blogpost](https://blog.radarr.video/development/update/2018/11/11/roadmap-update.html) for an overview of planned features.
|
||||
|
||||
#### [Feature Requests](http://feathub.com/Radarr/Radarr)
|
||||
|
||||
## Configuring the Development Environment
|
||||
|
||||
### Requirements
|
||||
|
||||
* [Visual Studio Community 2019](https://www.visualstudio.com/vs/community/) or [Rider](http://www.jetbrains.com/rider/)
|
||||
* [Visual Studio Community 2017](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/)
|
||||
|
||||
### Setup
|
||||
|
||||
* 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))
|
||||
* 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.
|
||||
* Grab the submodules `git submodule init && git submodule update`
|
||||
* Install the required Node Packages `npm install`
|
||||
* Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command.
|
||||
|
||||
> **Notice**
|
||||
> Gulp must be running at all times while you are working with Radarr client source files.
|
||||
@@ -111,9 +125,9 @@ Radarr is currently undergoing rapid development and pull requests are actively
|
||||
|
||||
### Development
|
||||
|
||||
* Open `Radarr.sln` in Visual Studio 2019 or run the build.sh script, if Mono is installed. Alternatively you can use Jetbrains Rider, since it works on all Platforms.
|
||||
* Open `NzbDrone.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, or build in VS
|
||||
* Run `build.sh` before running
|
||||
|
||||
## Supporters
|
||||
|
||||
@@ -134,7 +148,6 @@ This project would not be possible without the support by these amazing folks. [
|
||||
### 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/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/)
|
||||
@@ -143,4 +156,4 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrai
|
||||
## License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2020
|
||||
* Copyright 2010-2018
|
||||
|
||||
@@ -7,14 +7,15 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
majorVersion: '3.0.0'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
majorVersion: '0.2.0'
|
||||
minorVersion: $[counter('minorVersionOld', 1510)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '3.1.401'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
windowsInstaller: 'Radarr.$(buildName).windows-installer.exe'
|
||||
windowsZip: 'Radarr.$(buildName).windows.zip'
|
||||
macOsApp: 'Radarr.$(buildName).osx-app.zip'
|
||||
macOsTar: 'Radarr.$(buildName).osx.tar.gz'
|
||||
linuxTar: 'Radarr.$(buildName).linux.tar.gz'
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -27,32 +28,9 @@ pr:
|
||||
- aphrodite
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
displayName: Setup
|
||||
jobs:
|
||||
- job:
|
||||
displayName: Build Variables
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
# Set the build name properly. The 'name' property won't recursively expand so hack here:
|
||||
- bash: echo "##vso[build.updatebuildnumber]$RADARRVERSION"
|
||||
displayName: Set Build Name
|
||||
- bash: |
|
||||
if [[ $BUILD_REASON == "PullRequest" ]]; then
|
||||
git diff origin/aphrodite...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
|
||||
echo $? > not_backend_update
|
||||
else
|
||||
echo 0 > not_backend_update
|
||||
fi
|
||||
cat not_backend_update
|
||||
displayName: Check for Backend File Changes
|
||||
- publish: not_backend_update
|
||||
artifact: not_backend_update
|
||||
displayName: Publish update type
|
||||
- stage: Build_Backend
|
||||
displayName: Build Backend
|
||||
dependsOn: Setup
|
||||
|
||||
jobs:
|
||||
- job: Backend
|
||||
strategy:
|
||||
@@ -69,56 +47,42 @@ stages:
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
variables:
|
||||
# Disable stylecop here - linting errors get caught by the analyze task
|
||||
EnableAnalyzers: 'false'
|
||||
steps:
|
||||
# Set the build name properly. The 'name' property won't recursively expand so hack here:
|
||||
- powershell: Write-Host "##vso[build.updatebuildnumber]$($env:RADARRVERSION)"
|
||||
displayName: Set Build Name
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
- task: Assembly-Info-NetFramework@2
|
||||
displayName: Patch AssemblyInfo
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: ./build.sh --backend
|
||||
Path: '$(Build.SourcesDirectory)'
|
||||
FileNames: 'src/NzbDrone.Common/Properties/SharedAssemblyInfo.cs'
|
||||
InsertAttributes: false
|
||||
FileEncoding: 'auto'
|
||||
WriteBOM: false
|
||||
Configuration: 'debug'
|
||||
VersionNumber: '$(radarrVersion)'
|
||||
FileVersionNumber: '$(radarrVersion)'
|
||||
InformationalVersion: '$(radarrVersion)-rc1'
|
||||
- bash: ./build.sh --only-backend
|
||||
displayName: Build Radarr Backend
|
||||
- bash: |
|
||||
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
|
||||
find ${TESTSFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
|
||||
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
|
||||
displayName: Clean up intermediate output
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- publish: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
displayName: Publish Backend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/netcoreapp3.1/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/net462/linux-x64/publish'
|
||||
artifact: LinuxTests
|
||||
displayName: Publish Linux Mono Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/netcoreapp3.1/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/netcoreapp3.1/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- publish: '$(testsFolder)/netcoreapp3.1/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
- publish: $(testsFolder)
|
||||
artifact: '$(osName)Tests'
|
||||
displayName: Publish Test Package
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
||||
- stage: Build_Frontend
|
||||
displayName: Frontend
|
||||
dependsOn: Setup
|
||||
displayName: Build Frontend
|
||||
dependsOn: []
|
||||
|
||||
jobs:
|
||||
- job: Build
|
||||
- job: Frontend
|
||||
strategy:
|
||||
matrix:
|
||||
Linux:
|
||||
@@ -140,25 +104,16 @@ stages:
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --frontend
|
||||
- bash: ./build.sh --only-frontend
|
||||
displayName: Build Radarr Frontend
|
||||
env:
|
||||
FORCE_COLOR: 0
|
||||
YARN_CACHE_FOLDER: $(yarnCacheFolder)
|
||||
- publish: $(outputFolder)
|
||||
artifact: '$(osName)Frontend'
|
||||
displayName: Publish Frontend
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
|
||||
- stage: Installer
|
||||
|
||||
- stage: Package
|
||||
dependsOn:
|
||||
- Build_Backend
|
||||
- Build_Frontend
|
||||
@@ -182,21 +137,16 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
- bash: ./build.sh --only-packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
setup/inno/ISCC.exe setup/radarr.iss //DFramework=netcoreapp3.1
|
||||
cp setup/output/Radarr.*windows.netcoreapp3.1.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
./setup/inno/ISCC.exe "./setup/radarr.iss"
|
||||
cp ./setup/output/Radarr.*windows.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/${WINDOWSINSTALLER}
|
||||
displayName: Create Windows installer
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'WindowsInstaller'
|
||||
displayName: Publish Installer
|
||||
|
||||
- stage: Packages
|
||||
dependsOn:
|
||||
- Build_Backend
|
||||
- Build_Frontend
|
||||
jobs:
|
||||
- job: Other_Packages
|
||||
displayName: Create Standard Packages
|
||||
pool:
|
||||
@@ -216,158 +166,74 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
- bash: ./build.sh --only-packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
find . -name "Radarr" -exec chmod a+x {} \;
|
||||
find . -name "Radarr.Update" -exec chmod a+x {} \;
|
||||
displayName: Set executable bits
|
||||
chmod a+x $(artifactsFolder)/macos/Radarr/Radarr
|
||||
chmod a+x $(artifactsFolder)/macos-app/Radarr.app/Contents/MacOS/Radarr
|
||||
displayName: Set Mac executable bits
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Windows Core zip
|
||||
displayName: Create Windows zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/$(windowsZip)'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/windows/netcoreapp3.1
|
||||
rootFolderOrFile: $(artifactsFolder)/windows
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core app
|
||||
displayName: Create MacOS app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/$(macOsApp)'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos-app/netcoreapp3.1
|
||||
rootFolderOrFile: $(artifactsFolder)/macos-app
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create MacOS Core tar
|
||||
displayName: Create MacOS tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/$(macOsTar)'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/macos/netcoreapp3.1
|
||||
rootFolderOrFile: $(artifactsFolder)/macos
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Mono tar
|
||||
displayName: Create Linux tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux.tar.gz'
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/$(linuxTar)'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net462
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/netcoreapp3.1
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create Linux Musl Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/netcoreapp3.1
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/netcoreapp3.1
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/netcoreapp3.1
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/netcoreapp3.1
|
||||
rootFolderOrFile: $(artifactsFolder)/linux
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Packages'
|
||||
displayName: Publish Packages
|
||||
- bash: |
|
||||
echo "Uploading source maps to sentry"
|
||||
curl -sL https://sentry.io/get-cli/ | bash
|
||||
RELEASENAME="${RADARRVERSION}-${BUILD_SOURCEBRANCHNAME}"
|
||||
sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}"
|
||||
sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
|
||||
sentry-cli releases set-commits --auto "${RELEASENAME}"
|
||||
sentry-cli releases deploys "${RELEASENAME}" new -e aphrodite
|
||||
if [ $? -gt 0 ]; then
|
||||
echo "##vso[task.logissue type=warning]Error uploading source maps."
|
||||
fi
|
||||
exit 0
|
||||
displayName: Publish Sentry Source Maps
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/aphrodite'))
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
|
||||
SENTRY_ORG: $(sentryOrg)
|
||||
SENTRY_URL: $(sentryUrl)
|
||||
|
||||
- stage: Unit_Test
|
||||
displayName: Unit Tests
|
||||
dependsOn: Build_Backend
|
||||
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'not_backend_update'
|
||||
targetPath: '.'
|
||||
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
|
||||
name: setVar
|
||||
|
||||
- job: Unit
|
||||
displayName: Unit Native
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.14'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
imageName: 'windows-2019'
|
||||
LinuxCore:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
imageName: 'ubuntu-18.04'
|
||||
pattern: 'Radarr.**.linux-core-x64.tar.gz'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: WindowsTests
|
||||
targetPath: $(testsFolder)
|
||||
- bash: |
|
||||
wget https://mediaarea.net/repo/deb/repo-mediaarea_1.0-11_all.deb
|
||||
@@ -379,156 +245,62 @@ stages:
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
wget https://github.com/acoustid/chromaprint/releases/download/v1.4.3/chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz
|
||||
sudo tar xf chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz --strip-components=1 --directory /usr/bin
|
||||
displayName: Install fpcalc
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
|
||||
- bash: |
|
||||
SYMLINK=6_6_0
|
||||
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
|
||||
echo "##vso[task.setvariable variable=MONOPREFIX;]$MONOPREFIX"
|
||||
echo "##vso[task.setvariable variable=PKG_CONFIG_PATH;]$MONOPREFIX/lib/pkgconfig:$MONOPREFIX/share/pkgconfig:$PKG_CONFIG_PATH"
|
||||
echo "##vso[task.setvariable variable=PATH;]$MONOPREFIX/bin:$PATH"
|
||||
displayName: Set Mono Version
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
|
||||
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh ${OSNAME} Unit Test
|
||||
displayName: Run Tests
|
||||
env:
|
||||
TEST_DIR: $(Build.SourcesDirectory)/_tests
|
||||
displayName: Run Unit Tests
|
||||
- publish: TestResult.xml
|
||||
artifact: 'TestResult'
|
||||
displayName: Publish Test Result
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- job: Unit_Docker
|
||||
displayName: Unit Docker
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
strategy:
|
||||
matrix:
|
||||
mono520:
|
||||
testName: 'Mono 5.20'
|
||||
artifactName: LinuxTests
|
||||
containerImage: servarr/testimages:mono-5.20
|
||||
mono610:
|
||||
testName: 'Mono 6.10'
|
||||
artifactName: LinuxTests
|
||||
containerImage: servarr/testimages:mono-6.10
|
||||
mono612:
|
||||
testName: 'Mono 6.12'
|
||||
artifactName: LinuxTests
|
||||
containerImage: servarr/testimages:mono-6.12
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
containerImage: servarr/testimages:alpine
|
||||
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
timeoutInMinutes: 10
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: $(artifactName)
|
||||
targetPath: $(testsFolder)
|
||||
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
ls -lR ${TESTSFOLDER}
|
||||
${TESTSFOLDER}/test.sh Linux Unit Test
|
||||
displayName: Run Tests
|
||||
- task: PublishTestResults@2
|
||||
displayName: Publish Test Results
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
testRunTitle: '$(osName) Unit Tests'
|
||||
failTaskOnFailedTests: true
|
||||
|
||||
- stage: Integration
|
||||
displayName: Integration
|
||||
dependsOn: Packages
|
||||
dependsOn: Package
|
||||
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'not_backend_update'
|
||||
targetPath: '.'
|
||||
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
|
||||
name: setVar
|
||||
|
||||
- job: Integration_Native
|
||||
displayName: Integration Native
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
- job: Integration
|
||||
strategy:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.14'
|
||||
pattern: 'Radarr.**.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'WindowsCore'
|
||||
imageName: 'windows-2019'
|
||||
pattern: 'Radarr.**.windows-core-x64.zip'
|
||||
LinuxCore:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
testName: 'LinuxCore'
|
||||
imageName: 'ubuntu-18.04'
|
||||
pattern: 'Radarr.**.linux-core-x64.tar.gz'
|
||||
pattern: 'Radarr.**.linux.tar.gz'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
pattern: 'Radarr.**.osx.tar.gz'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
pattern: 'Radarr.**.windows.zip'
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
|
||||
steps:
|
||||
- bash: |
|
||||
SYMLINK=6_6_0
|
||||
SYMLINK=5_18_1
|
||||
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
|
||||
echo "##vso[task.setvariable variable=MONOPREFIX;]$MONOPREFIX"
|
||||
echo "##vso[task.setvariable variable=PKG_CONFIG_PATH;]$MONOPREFIX/lib/pkgconfig:$MONOPREFIX/share/pkgconfig:$PKG_CONFIG_PATH"
|
||||
echo "##vso[task.setvariable variable=PATH;]$MONOPREFIX/bin:$PATH"
|
||||
displayName: Set Mono Version
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)Tests'
|
||||
artifactName: WindowsTests
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -547,6 +319,7 @@ stages:
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
ls -lR
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh ${OSNAME} Integration Test
|
||||
displayName: Run Integration Tests
|
||||
@@ -554,86 +327,13 @@ stages:
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- job: Integration_Docker
|
||||
displayName: Integration Docker
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
strategy:
|
||||
matrix:
|
||||
mono520:
|
||||
testName: 'Mono 5.20'
|
||||
artifactName: LinuxTests
|
||||
containerImage: servarr/testimages:mono-5.20
|
||||
pattern: 'Radarr.**.linux.tar.gz'
|
||||
mono610:
|
||||
testName: 'Mono 6.10'
|
||||
artifactName: LinuxTests
|
||||
containerImage: servarr/testimages:mono-6.10
|
||||
pattern: 'Radarr.**.linux.tar.gz'
|
||||
mono612:
|
||||
testName: 'Mono 6.12'
|
||||
artifactName: LinuxTests
|
||||
containerImage: servarr/testimages:mono-6.12
|
||||
pattern: 'Radarr.**.linux.tar.gz'
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
containerImage: servarr/testimages:alpine
|
||||
pattern: 'Radarr.**.linux-musl-core-x64.tar.gz'
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
timeoutInMinutes: 15
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: $(artifactName)
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: Packages
|
||||
itemPattern: '**/$(pattern)'
|
||||
targetPath: $(Build.ArtifactStagingDirectory)
|
||||
- task: ExtractFiles@1
|
||||
inputs:
|
||||
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
|
||||
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
|
||||
displayName: Extract Package
|
||||
- bash: |
|
||||
mkdir -p ./bin/
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh Linux Integration Test
|
||||
displayName: Run Integration Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Integration Tests'
|
||||
testRunTitle: '$(osName) Integration Tests'
|
||||
failTaskOnFailedTests: true
|
||||
displayName: Publish Test Results
|
||||
|
||||
- stage: Automation
|
||||
displayName: Automation
|
||||
dependsOn: Packages
|
||||
dependsOn: Package
|
||||
|
||||
jobs:
|
||||
- job: Automation
|
||||
@@ -642,33 +342,29 @@ stages:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
pattern: 'Radarr.**.linux-core-x64.tar.gz'
|
||||
pattern: 'Radarr.**.linux.tar.gz'
|
||||
failBuild: true
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: 'macos-10.14'
|
||||
pattern: 'Radarr.**.osx-core-x64.tar.gz'
|
||||
imageName: 'macos-10.14' # Fails due to firefox not being installed on image
|
||||
pattern: 'Radarr.**.osx.tar.gz'
|
||||
failBuild: true
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
pattern: 'Radarr.**.windows-core-x64.zip'
|
||||
pattern: 'Radarr.**.windows.zip'
|
||||
failBuild: true
|
||||
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(osName)CoreTests'
|
||||
artifactName: WindowsTests
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -688,9 +384,9 @@ stages:
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
if [[ $OSNAME == "Mac" ]]; then
|
||||
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-macos.tar.gz
|
||||
url=https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-macos.tar.gz
|
||||
elif [[ $OSNAME == "Linux" ]]; then
|
||||
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-linux64.tar.gz
|
||||
url=https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz
|
||||
else
|
||||
echo "Unhandled OS"
|
||||
exit 1
|
||||
@@ -700,9 +396,10 @@ stages:
|
||||
mv geckodriver _tests
|
||||
displayName: Install Gecko Driver
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: ls -lR
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
|
||||
${TESTSFOLDER}/test.sh ${OSNAME} Automation
|
||||
displayName: Run Automation Tests
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
@@ -712,137 +409,8 @@ stages:
|
||||
failTaskOnFailedTests: $(failBuild)
|
||||
displayName: Publish Test Results
|
||||
|
||||
- stage: Analyze
|
||||
dependsOn:
|
||||
- Setup
|
||||
displayName: Analyze
|
||||
|
||||
jobs:
|
||||
- job: Prepare
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- checkout: none
|
||||
- task: DownloadPipelineArtifact@2
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'not_backend_update'
|
||||
targetPath: '.'
|
||||
- bash: echo "##vso[task.setvariable variable=backendNotUpdated;isOutput=true]$(cat not_backend_update)"
|
||||
name: setVar
|
||||
|
||||
- job: Lint_Frontend
|
||||
displayName: Lint Frontend
|
||||
strategy:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: 'ubuntu-18.04'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: 'windows-2019'
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Set Node.js version
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
- checkout: self
|
||||
submodules: true
|
||||
fetchDepth: 1
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'yarn | "$(osName)" | yarn.lock'
|
||||
restoreKeys: |
|
||||
yarn | "$(osName)"
|
||||
yarn
|
||||
path: $(yarnCacheFolder)
|
||||
displayName: Cache Yarn packages
|
||||
- bash: ./build.sh --lint
|
||||
displayName: Lint Radarr Frontend
|
||||
env:
|
||||
FORCE_COLOR: 0
|
||||
YARN_CACHE_FOLDER: $(yarnCacheFolder)
|
||||
|
||||
- job: Analyze_Frontend
|
||||
displayName: Frontend
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@1
|
||||
env:
|
||||
SONAR_SCANNER_OPTS: ''
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'radarr'
|
||||
scannerMode: 'CLI'
|
||||
configMode: 'manual'
|
||||
cliProjectKey: 'Radarr_Radarr.UI'
|
||||
cliProjectName: 'RadarrUI'
|
||||
cliProjectVersion: '$(radarrVersion)'
|
||||
cliSources: './frontend'
|
||||
- task: SonarCloudAnalyze@1
|
||||
|
||||
- job: Analyze_Backend
|
||||
displayName: Backend
|
||||
dependsOn: Prepare
|
||||
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
|
||||
|
||||
variables:
|
||||
disable.coverage.autogenerate: 'true'
|
||||
|
||||
pool:
|
||||
vmImage: windows-2019
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
submodules: true
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
- task: SonarCloudPrepare@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
inputs:
|
||||
SonarCloud: 'SonarCloud'
|
||||
organization: 'radarr'
|
||||
scannerMode: 'MSBuild'
|
||||
projectKey: 'Radarr_Radarr'
|
||||
projectName: 'Radarr'
|
||||
projectVersion: '$(radarrVersion)'
|
||||
extraProperties: |
|
||||
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
|
||||
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*,**/NzbDrone.Api/**/*,**/MonoTorrent/**/*,**/Marr.Data/**/*
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f netcoreapp3.1 -r win-x64
|
||||
TEST_DIR=_tests/netcoreapp3.1/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
- task: SonarCloudAnalyze@1
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
displayName: Publish SonarCloud Results
|
||||
- task: reportgenerator@4
|
||||
displayName: Generate Coverage Report
|
||||
inputs:
|
||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||
targetdir: '$(Build.SourcesDirectory)/CoverageResults/combined'
|
||||
reporttypes: 'HtmlInline_AzurePipelines;Cobertura;Badges'
|
||||
- task: PublishCodeCoverageResults@1
|
||||
displayName: Publish Coverage Report
|
||||
inputs:
|
||||
codeCoverageTool: 'cobertura'
|
||||
summaryFileLocation: './CoverageResults/combined/Cobertura.xml'
|
||||
reportDirectory: './CoverageResults/combined/'
|
||||
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
- Analyze
|
||||
- Unit_Test
|
||||
- Integration
|
||||
- Automation
|
||||
@@ -861,4 +429,3 @@ stages:
|
||||
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
|
||||
DISCORDCHANNELID: $(discordChannelId)
|
||||
DISCORDWEBHOOKKEY: $(discordWebhookKey)
|
||||
|
||||
|
||||
459
build.sh
@@ -1,9 +1,35 @@
|
||||
#! /bin/bash
|
||||
set -e
|
||||
msBuildVersion='15.0'
|
||||
outputFolder='./_output'
|
||||
outputFolderLinux='./_output_linux'
|
||||
outputFolderMacOS='./_output_macos'
|
||||
outputFolderMacOSApp='./_output_macos_app'
|
||||
testPackageFolder='./_tests/'
|
||||
testSearchPattern='*.Test/bin/x86/Release/*'
|
||||
sourceFolder='./src'
|
||||
slnFile=$sourceFolder/NzbDrone.sln
|
||||
updateFolder=$outputFolder/NzbDrone.Update
|
||||
updateFolderMono=$outputFolderLinux/NzbDrone.Update
|
||||
|
||||
outputFolder='_output'
|
||||
testPackageFolder='_tests'
|
||||
artifactsFolder="_artifacts";
|
||||
#Artifact variables
|
||||
artifactsFolder="./_artifacts";
|
||||
artifactsFolderWindows=$artifactsFolder/windows
|
||||
artifactsFolderLinux=$artifactsFolder/linux
|
||||
artifactsFolderMacOS=$artifactsFolder/macos
|
||||
artifactsFolderMacOSApp=$artifactsFolder/macos-app
|
||||
|
||||
nuget='tools/nuget/nuget.exe';
|
||||
vswhere='tools/vswhere/vswhere.exe';
|
||||
|
||||
CheckExitCode()
|
||||
{
|
||||
"$@"
|
||||
local status=$?
|
||||
if [ $status -ne 0 ]; then
|
||||
echo "error with $1" >&2
|
||||
exit 1
|
||||
fi
|
||||
return $status
|
||||
}
|
||||
|
||||
ProgressStart()
|
||||
{
|
||||
@@ -15,29 +41,53 @@ ProgressEnd()
|
||||
echo "Finish '$1'"
|
||||
}
|
||||
|
||||
UpdateVersionNumber()
|
||||
CleanFolder()
|
||||
{
|
||||
if [ "$RADARRVERSION" != "" ]; then
|
||||
echo "Updating Version Info"
|
||||
sed -i'' -e "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$RADARRVERSION<\/AssemblyVersion>/g" src/Directory.Build.props
|
||||
sed -i'' -e "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BUILD_SOURCEBRANCHNAME}<\/AssemblyConfiguration>/g" src/Directory.Build.props
|
||||
sed -i'' -e "s/<string>10.0.0.0<\/string>/<string>$RADARRVERSION<\/string>/g" macOS/Radarr.app/Contents/Info.plist
|
||||
local path=$1
|
||||
local keepConfigFiles=$2
|
||||
|
||||
find $path -name "*.transform" -exec rm "{}" \;
|
||||
|
||||
if [ $keepConfigFiles != true ] ; then
|
||||
find $path -name "*.dll.config" -exec rm "{}" \;
|
||||
fi
|
||||
|
||||
echo "Removing FluentValidation.Resources files"
|
||||
find $path -name "FluentValidation.resources.dll" -exec rm "{}" \;
|
||||
find $path -name "App.config" -exec rm "{}" \;
|
||||
|
||||
echo "Removing vshost files"
|
||||
find $path -name "*.vshost.exe" -exec rm "{}" \;
|
||||
|
||||
echo "Removing dylib files"
|
||||
find $path -name "*.dylib" -exec rm "{}" \;
|
||||
|
||||
echo "Removing Empty folders"
|
||||
find $path -depth -empty -type d -exec rm -r "{}" \;
|
||||
}
|
||||
|
||||
LintUI()
|
||||
BuildWithMSBuild()
|
||||
{
|
||||
ProgressStart 'ESLint'
|
||||
yarn lint
|
||||
ProgressEnd 'ESLint'
|
||||
msBuildPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -find MSBuild\\\\\*\*\\\\Bin\\\\MSBuild.exe`
|
||||
msBuildPath=${msBuildPath/C:\\/\/c\/}
|
||||
msBuildPath=${msBuildPath//\\/\/}
|
||||
msBuildDir=$(dirname "$msBuildPath")
|
||||
echo $msBuildPath
|
||||
|
||||
ProgressStart 'Stylelint'
|
||||
if [ "$os" = "windows" ]; then
|
||||
yarn stylelint-windows
|
||||
else
|
||||
yarn stylelint-linux
|
||||
fi
|
||||
ProgressEnd 'Stylelint'
|
||||
export PATH=$msBuildDir:$PATH
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Clean //m
|
||||
$nuget restore $slnFile
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
|
||||
}
|
||||
|
||||
BuildWithXbuild()
|
||||
{
|
||||
export MONO_IOMAP=case
|
||||
CheckExitCode msbuild /p:Configuration=Debug /t:Clean $slnFile
|
||||
CheckExitCode msbuild /p:Configuration=Release /t:Clean $slnFile
|
||||
mono $nuget locals all -clear
|
||||
mono $nuget restore $slnFile
|
||||
CheckExitCode msbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||
}
|
||||
|
||||
Build()
|
||||
@@ -47,260 +97,217 @@ Build()
|
||||
rm -rf $outputFolder
|
||||
rm -rf $testPackageFolder
|
||||
|
||||
slnFile=src/Radarr.sln
|
||||
|
||||
if [ $os = "windows" ]; then
|
||||
platform=Windows
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
BuildWithMSBuild
|
||||
else
|
||||
platform=Posix
|
||||
BuildWithXbuild
|
||||
fi
|
||||
|
||||
dotnet clean $slnFile -c Debug
|
||||
dotnet clean $slnFile -c Release
|
||||
CleanFolder $outputFolder false
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
|
||||
else
|
||||
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
|
||||
fi
|
||||
echo "Removing Mono.Posix.dll"
|
||||
rm $outputFolder/Mono.Posix.dll
|
||||
|
||||
echo "Adding LICENSE.md"
|
||||
cp LICENSE.md $outputFolder
|
||||
|
||||
ProgressEnd 'Build'
|
||||
}
|
||||
|
||||
YarnInstall()
|
||||
{
|
||||
ProgressStart 'yarn install'
|
||||
yarn install --frozen-lockfile --network-timeout 120000
|
||||
ProgressEnd 'yarn install'
|
||||
}
|
||||
|
||||
RunGulp()
|
||||
{
|
||||
ProgressStart 'npm install'
|
||||
npm-cache install npm || CheckExitCode npm install
|
||||
ProgressEnd 'npm install'
|
||||
|
||||
ProgressStart 'Running gulp'
|
||||
yarn run build --production
|
||||
CheckExitCode npm run build
|
||||
ProgressEnd 'Running gulp'
|
||||
}
|
||||
|
||||
PackageFiles()
|
||||
PackageMono()
|
||||
{
|
||||
local folder="$1"
|
||||
local framework="$2"
|
||||
local runtime="$3"
|
||||
ProgressStart 'Creating Mono Package'
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
cp -r $outputFolder/$framework/$runtime/publish/* $folder
|
||||
cp -r $outputFolder/Radarr.Update/$framework/$runtime/publish $folder/Radarr.Update
|
||||
cp -r $outputFolder/UI $folder
|
||||
rm -rf $outputFolderLinux
|
||||
|
||||
echo "Adding LICENSE"
|
||||
cp LICENSE $folder
|
||||
}
|
||||
|
||||
PackageLinux()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating $runtime Package for $framework"
|
||||
|
||||
local folder=$artifactsFolder/$runtime/$framework/Radarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
echo "Copying Binaries"
|
||||
cp -r $outputFolder $outputFolderLinux
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
rm -f $folder/ServiceInstall.*
|
||||
rm -f $outputFolderLinux/ServiceUninstall.*
|
||||
rm -f $outputFolderLinux/ServiceInstall.*
|
||||
|
||||
echo "Removing Radarr.Windows"
|
||||
rm $folder/Radarr.Windows.*
|
||||
echo "Removing native windows binaries Sqlite, MediaInfo"
|
||||
rm -f $outputFolderLinux/sqlite3.*
|
||||
rm -f $outputFolderLinux/MediaInfo.*
|
||||
|
||||
echo "Adding Radarr.Mono to UpdatePackage"
|
||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||
if [ "$framework" = "netcoreapp3.1" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||
fi
|
||||
echo "Adding NzbDrone.Core.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Core/NzbDrone.Core.dll.config $outputFolderLinux
|
||||
|
||||
ProgressEnd "Creating $runtime Package for $framework"
|
||||
|
||||
echo "Renaming Radarr.Console.exe to Radarr.exe"
|
||||
rm $outputFolderLinux/Radarr.exe*
|
||||
for file in $outputFolderLinux/Radarr.Console.exe*; do
|
||||
mv "$file" "${file//.Console/}"
|
||||
done
|
||||
|
||||
echo "Removing NzbDrone.Windows"
|
||||
rm $outputFolderLinux/NzbDrone.Windows.*
|
||||
|
||||
echo "Adding NzbDrone.Mono to UpdatePackage"
|
||||
cp $outputFolderLinux/NzbDrone.Mono.* $updateFolderMono
|
||||
|
||||
ProgressEnd 'Creating Mono Package'
|
||||
}
|
||||
|
||||
PackageMacOS()
|
||||
{
|
||||
local framework="$1"
|
||||
|
||||
ProgressStart "Creating MacOS Package for $framework"
|
||||
ProgressStart 'Creating MacOS Package'
|
||||
|
||||
local folder=$artifactsFolder/macos/$framework/Radarr
|
||||
rm -rf $outputFolderMacOS
|
||||
mkdir $outputFolderMacOS
|
||||
|
||||
PackageFiles "$folder" "$framework" "osx-x64"
|
||||
echo "Adding Startup script"
|
||||
cp ./macOS/Radarr $outputFolderMacOS
|
||||
|
||||
if [ "$framework" = "net462" ]; then
|
||||
echo "Adding Startup script"
|
||||
cp macOS/Radarr $folder
|
||||
fi
|
||||
echo "Copying Binaries"
|
||||
cp -r $outputFolderLinux/* $outputFolderMacOS
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
rm -f $folder/ServiceInstall.*
|
||||
echo "Adding sqlite dylibs"
|
||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
|
||||
|
||||
echo "Removing Radarr.Windows"
|
||||
rm $folder/Radarr.Windows.*
|
||||
|
||||
echo "Adding Radarr.Mono to UpdatePackage"
|
||||
cp $folder/Radarr.Mono.* $folder/Radarr.Update
|
||||
if [ "$framework" = "netcoreapp3.1" ]; then
|
||||
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
|
||||
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
|
||||
fi
|
||||
echo "Adding MediaInfo dylib"
|
||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOS
|
||||
|
||||
ProgressEnd 'Creating MacOS Package'
|
||||
}
|
||||
|
||||
PackageMacOSApp()
|
||||
{
|
||||
local framework="$1"
|
||||
|
||||
ProgressStart "Creating macOS App Package for $framework"
|
||||
ProgressStart 'Creating macOS App Package'
|
||||
|
||||
local folder=$artifactsFolder/macos-app/$framework
|
||||
rm -rf $outputFolderMacOSApp
|
||||
mkdir $outputFolderMacOSApp
|
||||
cp -r ./macOS/Radarr.app $outputFolderMacOSApp
|
||||
mkdir -p $outputFolderMacOSApp/Radarr.app/Contents/MacOS
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
cp -r macOS/Radarr.app $folder
|
||||
mkdir -p $folder/Radarr.app/Contents/MacOS
|
||||
echo "Adding Startup script"
|
||||
cp ./macOS/Radarr $outputFolderMacOSApp/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $artifactsFolder/macos/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
|
||||
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Adding sqlite dylibs"
|
||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Adding MediaInfo dylib"
|
||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Radarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update
|
||||
rm -r $outputFolderMacOSApp/Radarr.app/Contents/MacOS/NzbDrone.Update
|
||||
|
||||
ProgressEnd 'Creating macOS App Package'
|
||||
}
|
||||
|
||||
PackageWindows()
|
||||
{
|
||||
local framework="$1"
|
||||
|
||||
ProgressStart "Creating Windows Package for $framework"
|
||||
|
||||
local folder=$artifactsFolder/windows/$framework/Radarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "win-x64"
|
||||
|
||||
echo "Removing Radarr.Mono"
|
||||
rm -f $folder/Radarr.Mono.*
|
||||
rm -f $folder/Mono.Posix.NETStandard.*
|
||||
rm -f $folder/libMonoPosixHelper.*
|
||||
|
||||
echo "Adding Radarr.Windows to UpdatePackage"
|
||||
cp $folder/Radarr.Windows.* $folder/Radarr.Update
|
||||
|
||||
ProgressEnd 'Creating Windows Package'
|
||||
}
|
||||
|
||||
Package()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
local SPLIT
|
||||
|
||||
IFS='-' read -ra SPLIT <<< "$runtime"
|
||||
|
||||
case "${SPLIT[0]}" in
|
||||
linux)
|
||||
PackageLinux "$framework" "$runtime"
|
||||
;;
|
||||
win)
|
||||
PackageWindows "$framework"
|
||||
;;
|
||||
osx)
|
||||
PackageMacOS "$framework"
|
||||
PackageMacOSApp "$framework"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
ProgressStart 'Creating Test Package'
|
||||
|
||||
cp test.sh "$testPackageFolder/$framework/$runtime/publish"
|
||||
rm -rf $testPackageFolder
|
||||
mkdir $testPackageFolder
|
||||
|
||||
rm -f $testPackageFolder/$framework/$runtime/*.log.config
|
||||
find . -maxdepth 6 -path $testSearchPattern -exec cp -r "{}" $testPackageFolder \;
|
||||
|
||||
# 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.27.0/geckodriver-v0.27.0-win64.zip"
|
||||
unzip -o gecko.zip
|
||||
cp geckodriver.exe "$testPackageFolder/$framework/win-x64/publish"
|
||||
if [ $runtime = "dotnet" ] ; then
|
||||
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolder
|
||||
else
|
||||
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolder
|
||||
fi
|
||||
|
||||
cp $outputFolder/*.dll $testPackageFolder
|
||||
cp $outputFolder/*.exe $testPackageFolder
|
||||
cp ./test.sh $testPackageFolder
|
||||
|
||||
rm -f $testPackageFolder/*.log.config
|
||||
|
||||
CleanFolder $testPackageFolder true
|
||||
|
||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder
|
||||
|
||||
echo "Copying CurlSharp libraries"
|
||||
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
|
||||
|
||||
echo "Adding sqlite and mediainfo dylibs"
|
||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $testPackageFolder
|
||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $testPackageFolder
|
||||
|
||||
ProgressEnd 'Creating Test Package'
|
||||
}
|
||||
|
||||
CleanupWindowsPackage()
|
||||
{
|
||||
ProgressStart 'Cleaning Windows Package'
|
||||
|
||||
echo "Removing NzbDrone.Mono"
|
||||
rm -f $outputFolder/NzbDrone.Mono.*
|
||||
|
||||
echo "Adding NzbDrone.Windows to UpdatePackage"
|
||||
cp $outputFolder/NzbDrone.Windows.* $updateFolder
|
||||
|
||||
ProgressEnd 'Cleaning Windows Package'
|
||||
}
|
||||
|
||||
PackageArtifacts()
|
||||
{
|
||||
echo "Creating Artifact Directories"
|
||||
|
||||
rm -rf $artifactsFolder
|
||||
mkdir $artifactsFolder
|
||||
|
||||
mkdir $artifactsFolderWindows
|
||||
mkdir $artifactsFolderMacOS
|
||||
mkdir $artifactsFolderLinux
|
||||
mkdir $artifactsFolderWindows/Radarr
|
||||
mkdir $artifactsFolderMacOS/Radarr
|
||||
mkdir $artifactsFolderLinux/Radarr
|
||||
mkdir $artifactsFolderMacOSApp
|
||||
|
||||
cp -r $outputFolder/* $artifactsFolderWindows/Radarr
|
||||
cp -r $outputFolderMacOSApp/* $artifactsFolderMacOSApp
|
||||
cp -r $outputFolderMacOS/* $artifactsFolderMacOS/Radarr
|
||||
cp -r $outputFolderLinux/* $artifactsFolderLinux/Radarr
|
||||
}
|
||||
|
||||
# Use mono or .net depending on OS
|
||||
case "$(uname -s)" in
|
||||
CYGWIN*|MINGW32*|MINGW64*|MSYS*)
|
||||
# on windows, use dotnet
|
||||
os="windows"
|
||||
runtime="dotnet"
|
||||
;;
|
||||
*)
|
||||
# otherwise use mono
|
||||
os="posix"
|
||||
runtime="mono"
|
||||
;;
|
||||
esac
|
||||
|
||||
POSITIONAL=()
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "No arguments provided, building everything"
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
LINT=YES
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
do
|
||||
key="$1"
|
||||
|
||||
case $key in
|
||||
--backend)
|
||||
BACKEND=YES
|
||||
--only-backend)
|
||||
ONLY_BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
RID="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
-f|--framework)
|
||||
FRAMEWORK="$2"
|
||||
shift # past argument
|
||||
shift # past value
|
||||
;;
|
||||
--frontend)
|
||||
FRONTEND=YES
|
||||
--only-frontend)
|
||||
ONLY_FRONTEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--packages)
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--all)
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
LINT=YES
|
||||
--only-packages)
|
||||
ONLY_PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
*) # unknown option
|
||||
@@ -311,53 +318,25 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
# Only build backend if we haven't set only-frontend or only-packages
|
||||
if [ -z "$ONLY_FRONTEND" ] && [ -z "$ONLY_PACKAGES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "netcoreapp3.1" "win-x64"
|
||||
PackageTests "netcoreapp3.1" "linux-x64"
|
||||
PackageTests "netcoreapp3.1" "linux-musl-x64"
|
||||
PackageTests "netcoreapp3.1" "osx-x64"
|
||||
PackageTests "net462" "linux-x64"
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
PackageTests
|
||||
fi
|
||||
|
||||
if [ "$FRONTEND" = "YES" ];
|
||||
# Only build frontend if we haven't set only-backend or only-packages
|
||||
if [ -z "$ONLY_BACKEND" ] && [ -z "$ONLY_PACKAGES" ];
|
||||
then
|
||||
YarnInstall
|
||||
RunGulp
|
||||
RunGulp
|
||||
fi
|
||||
|
||||
if [ "$LINT" = "YES" ];
|
||||
# Only package if we haven't set only-backend or only-frontend
|
||||
if [ -z "$ONLY_BACKEND" ] && [ -z "$ONLY_FRONTEND" ];
|
||||
then
|
||||
if [ -z "$FRONTEND" ];
|
||||
then
|
||||
YarnInstall
|
||||
fi
|
||||
|
||||
LintUI
|
||||
fi
|
||||
|
||||
if [ "$PACKAGES" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "netcoreapp3.1" "win-x64"
|
||||
Package "netcoreapp3.1" "linux-x64"
|
||||
Package "netcoreapp3.1" "linux-musl-x64"
|
||||
Package "netcoreapp3.1" "linux-arm64"
|
||||
Package "netcoreapp3.1" "linux-musl-arm64"
|
||||
Package "netcoreapp3.1" "linux-arm"
|
||||
Package "netcoreapp3.1" "osx-x64"
|
||||
Package "net462" "linux-x64"
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
PackageMono
|
||||
PackageMacOS
|
||||
PackageMacOSApp
|
||||
CleanupWindowsPackage
|
||||
PackageArtifacts
|
||||
fi
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
{{#versions}}
|
||||
## {{{label}}}
|
||||
|
||||
{{#sections}}
|
||||
### {{{label}}}
|
||||
{{#commits}}
|
||||
- {{{subject}}} [<a href="https://github.com/{{{author}}}">{{{author}}}</a>]
|
||||
{{/commits}}
|
||||
|
||||
{{/sections}}
|
||||
|
||||
{{/versions}}
|
||||
@@ -1,15 +0,0 @@
|
||||
**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.
|
||||
@@ -1,44 +0,0 @@
|
||||
input1 = """Prometheus.Special.Edition.Fan Edit.2012..BRRip.x264.AAC-m2g
|
||||
Star Wars Episode IV - A New Hope (Despecialized) 1999.mkv
|
||||
Prometheus.(Special.Edition.Remastered).2012.[Bluray-1080p].mkv
|
||||
Prometheus Extended 2012
|
||||
Prometheus Extended Directors Cut Fan Edit 2012
|
||||
Prometheus Director's Cut 2012
|
||||
Prometheus Directors Cut 2012
|
||||
Prometheus.(Extended.Theatrical.Version.IMAX).BluRay.1080p.2012.asdf
|
||||
2001 A Space Odyssey Director's Cut (1968).mkv
|
||||
2001: A Space Odyssey (Extended Directors Cut FanEdit) Bluray 1080p 1968
|
||||
A Fake Movie 2035 Directors 2012.mkv
|
||||
Blade Runner Director's Cut 2049.mkv
|
||||
Prometheus 50th Anniversary Edition 2012.mkv
|
||||
Movie 2in1 2012.mkv
|
||||
Movie IMAX 2012.mkv"""
|
||||
|
||||
output1 = """Special.Edition.Fan Edit BRRip.x264.AAC-m2g
|
||||
Despecialized mkv
|
||||
Special.Edition.Remastered Bluray-1080p].mkv
|
||||
Extended mkv
|
||||
Extended Directors Cut Fan Edit mkv
|
||||
Director's Cut mkv
|
||||
Directors Cut mkv
|
||||
Extended.Theatrical.Version.IMAX asdf
|
||||
Director's Cut mkv
|
||||
Extended Directors Cut FanEdit mkv
|
||||
Directors mkv
|
||||
Director's Cut mkv
|
||||
50th Anniversary Edition mkv
|
||||
2in1 mkv
|
||||
IMAX mkv"""
|
||||
|
||||
inputs = input1.split("\n")
|
||||
outputs = output1.split("\n")
|
||||
real_o = []
|
||||
for output in outputs:
|
||||
real_o.append(output.split(" ")[0].replace(".", " ").strip())
|
||||
|
||||
count = 0
|
||||
|
||||
for inp in inputs:
|
||||
o = real_o[count]
|
||||
print "[TestCase(\"{0}\", \"{1}\")]".format(inp, o)
|
||||
count += 1
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"remove-empty-rulesets": true,
|
||||
"always-semicolon": true,
|
||||
"color-case": "lower",
|
||||
"block-indent": " ",
|
||||
"color-shorthand": false,
|
||||
"element-case": "lower",
|
||||
"eof-newline": true,
|
||||
"leading-zero": true,
|
||||
"quotes": "double",
|
||||
"sort-order-fallback": "abc",
|
||||
"space-before-colon": "",
|
||||
"space-after-colon": " ",
|
||||
"space-before-combinator": " ",
|
||||
"space-after-combinator": " ",
|
||||
"space-between-declarations": "\n",
|
||||
"space-before-opening-brace": " ",
|
||||
"space-after-opening-brace": "\n",
|
||||
"space-after-selector-delimiter": " ",
|
||||
"space-before-selector-delimiter": "",
|
||||
"space-before-closing-brace": "\n",
|
||||
"strip-spaces": true,
|
||||
"tab-size": true,
|
||||
"unitless-zero": false
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
{
|
||||
"indent": {
|
||||
"value": " ",
|
||||
"FunctionExpression": 1,
|
||||
"ArrayExpression": 1,
|
||||
"ObjectExpression": 1
|
||||
},
|
||||
"lineBreak": {
|
||||
"value": "\n",
|
||||
|
||||
"before": {
|
||||
"ArrayPatternClosing": 0,
|
||||
"ArrayPatternComma": 0,
|
||||
"ArrayPatternOpening": 0,
|
||||
"ArrowFunctionExpressionArrow": 0,
|
||||
"ArrowFunctionExpressionClosingBrace": ">=1",
|
||||
"ArrowFunctionExpressionOpeningBrace": 0,
|
||||
"AssignmentExpression": ">=1",
|
||||
"AssignmentOperator": 0,
|
||||
"BlockStatement": 0,
|
||||
"BreakKeyword": ">=1",
|
||||
"CallExpression": -1,
|
||||
"CallExpressionClosingParentheses": -1,
|
||||
"CallExpressionOpeningParentheses": 0,
|
||||
"CatchClosingBrace": ">=1",
|
||||
"CatchKeyword": 0,
|
||||
"CatchOpeningBrace": 0,
|
||||
"ClassDeclaration": ">=1",
|
||||
"ClassDeclarationClosingBrace": ">=1",
|
||||
"ClassDeclarationOpeningBrace": 0,
|
||||
"ConditionalExpression": ">=1",
|
||||
"DeleteOperator": ">=1",
|
||||
"DoWhileStatement": ">=1",
|
||||
"DoWhileStatementClosingBrace": ">=1",
|
||||
"DoWhileStatementOpeningBrace": 0,
|
||||
"ElseIfStatement": 0,
|
||||
"ElseIfStatementClosingBrace": ">=1",
|
||||
"ElseIfStatementOpeningBrace": 0,
|
||||
"ElseStatement": 0,
|
||||
"ElseStatementClosingBrace": ">=1",
|
||||
"ElseStatementOpeningBrace": 0,
|
||||
"EmptyStatement": -1,
|
||||
"EndOfFile": -1,
|
||||
"FinallyClosingBrace": ">=1",
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": 0,
|
||||
"ForInStatement": ">=1",
|
||||
"ForInStatementClosingBrace": ">=1",
|
||||
"ForInStatementExpressionClosing": 0,
|
||||
"ForInStatementExpressionOpening": 0,
|
||||
"ForInStatementOpeningBrace": 0,
|
||||
"ForStatement": ">=1",
|
||||
"ForStatementClosingBrace": ">=1",
|
||||
"ForStatementExpressionClosing": "<2",
|
||||
"ForStatementExpressionOpening": 0,
|
||||
"ForStatementOpeningBrace": 0,
|
||||
"FunctionDeclaration": ">=1",
|
||||
"FunctionDeclarationClosingBrace": ">=1",
|
||||
"FunctionDeclarationOpeningBrace": 0,
|
||||
"FunctionExpression": 0,
|
||||
"FunctionExpressionClosingBrace": 1,
|
||||
"FunctionExpressionOpeningBrace":0,
|
||||
"IIFEClosingParentheses": 0,
|
||||
"IfStatement": ">=1",
|
||||
"IfStatementClosingBrace": ">=1",
|
||||
"IfStatementOpeningBrace": 0,
|
||||
"LogicalExpression": -1,
|
||||
"MemberExpressionClosing": 0,
|
||||
"MemberExpressionOpening": 0,
|
||||
"MemberExpressionPeriod": -1,
|
||||
"MethodDefinition": ">=1",
|
||||
"ObjectExpressionClosingBrace": "<=1",
|
||||
"ObjectPatternClosingBrace": 0,
|
||||
"ObjectPatternComma": 0,
|
||||
"ObjectPatternOpeningBrace": 0,
|
||||
"ParameterDefault": 0,
|
||||
"Property": "<=2",
|
||||
"PropertyValue": 0,
|
||||
"ReturnStatement": -1,
|
||||
"SwitchClosingBrace": ">=1",
|
||||
"SwitchOpeningBrace": 0,
|
||||
"ThisExpression": -1,
|
||||
"ThrowStatement": ">=1",
|
||||
"TryClosingBrace": ">=1",
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": 0,
|
||||
"VariableDeclaration": ">=1",
|
||||
"VariableDeclarationSemiColon": 0,
|
||||
"VariableDeclarationWithoutInit": ">=1",
|
||||
"VariableName": ">=1",
|
||||
"VariableValue": 0,
|
||||
"WhileStatement": ">=1",
|
||||
"WhileStatementClosingBrace": ">=1",
|
||||
"WhileStatementOpeningBrace": 0
|
||||
},
|
||||
|
||||
"after": {
|
||||
"ArrayPatternClosing": 0,
|
||||
"ArrayPatternComma": 0,
|
||||
"ArrayPatternOpening": 0,
|
||||
"ArrowFunctionExpressionArrow": 0,
|
||||
"ArrowFunctionExpressionClosingBrace": -1,
|
||||
"ArrowFunctionExpressionOpeningBrace": ">=1",
|
||||
"AssignmentExpression": ">=1",
|
||||
"AssignmentOperator": 0,
|
||||
"BlockStatement": 0,
|
||||
"BreakKeyword": -1,
|
||||
"CallExpression": -1,
|
||||
"CallExpressionClosingParentheses": -1,
|
||||
"CallExpressionOpeningParentheses": -1,
|
||||
"CatchClosingBrace": ">=0",
|
||||
"CatchKeyword": 0,
|
||||
"CatchOpeningBrace": ">=1",
|
||||
"ClassDeclaration": ">=1",
|
||||
"ClassDeclarationClosingBrace": ">=1",
|
||||
"ClassDeclarationOpeningBrace": ">=1",
|
||||
"ConditionalExpression": ">=1",
|
||||
"DeleteOperator": ">=1",
|
||||
"DoWhileStatement": ">=1",
|
||||
"DoWhileStatementClosingBrace": 0,
|
||||
"DoWhileStatementOpeningBrace": ">=1",
|
||||
"ElseIfStatement": ">=1",
|
||||
"ElseIfStatementClosingBrace": ">=1",
|
||||
"ElseIfStatementOpeningBrace": ">=1",
|
||||
"ElseStatement": ">=1",
|
||||
"ElseStatementClosingBrace": ">=1",
|
||||
"ElseStatementOpeningBrace": ">=1",
|
||||
"EmptyStatement": -1,
|
||||
"FinallyClosingBrace": ">=1",
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": ">=1",
|
||||
"ForInStatement": ">=1",
|
||||
"ForInStatementClosingBrace": ">=1",
|
||||
"ForInStatementExpressionClosing": -1,
|
||||
"ForInStatementExpressionOpening": "<2",
|
||||
"ForInStatementOpeningBrace": ">=1",
|
||||
"ForStatement": ">=1",
|
||||
"ForStatementClosingBrace": ">=1",
|
||||
"ForStatementExpressionClosing": -1,
|
||||
"ForStatementExpressionOpening": "<2",
|
||||
"ForStatementOpeningBrace": ">=1",
|
||||
"FunctionDeclaration": ">=1",
|
||||
"FunctionDeclarationClosingBrace": ">=1",
|
||||
"FunctionDeclarationOpeningBrace": ">=1",
|
||||
"FunctionExpression": 0,
|
||||
"FunctionExpressionClosingBrace": -1,
|
||||
"FunctionExpressionOpeningBrace": 1,
|
||||
"IIFEOpeningParentheses": 0,
|
||||
"IfStatement": ">=1",
|
||||
"IfStatementClosingBrace": ">=1",
|
||||
"IfStatementOpeningBrace": ">=1",
|
||||
"LogicalExpression": -1,
|
||||
"MemberExpressionClosing": 0,
|
||||
"MemberExpressionOpening": 0,
|
||||
"MemberExpressionPeriod": 0,
|
||||
"MethodDefinition": ">=1",
|
||||
"ObjectExpressionOpeningBrace": "<=1",
|
||||
"ObjectPatternClosingBrace": 0,
|
||||
"ObjectPatternComma": 0,
|
||||
"ObjectPatternOpeningBrace": 0,
|
||||
"ParameterDefault": 0,
|
||||
"Property": -1,
|
||||
"PropertyName": 0,
|
||||
"ReturnStatement": -1,
|
||||
"SwitchCaseColon": ">=1",
|
||||
"SwitchClosingBrace": ">=1",
|
||||
"SwitchOpeningBrace": ">=1",
|
||||
"ThisExpression": 0,
|
||||
"ThrowStatement": ">=1",
|
||||
"TryClosingBrace": 0,
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": ">=1",
|
||||
"VariableDeclaration": ">=1",
|
||||
"VariableDeclarationSemiColon": ">=1",
|
||||
"VariableValue": -1,
|
||||
"WhileStatement": ">=1",
|
||||
"WhileStatementClosingBrace": ">=1",
|
||||
"WhileStatementOpeningBrace": ">=1"
|
||||
}
|
||||
},
|
||||
"whiteSpace": {
|
||||
"value": " ",
|
||||
"removeTrailing": 1,
|
||||
"before": {
|
||||
"ArgumentComma": 0,
|
||||
"ArgumentList": 0,
|
||||
"ArgumentListArrayExpression": 0,
|
||||
"ArgumentListFunctionExpression": 1,
|
||||
"ArgumentListObjectExpression": 0,
|
||||
"ArrayExpressionClosing": 0,
|
||||
"ArrayExpressionComma": 0,
|
||||
"ArrayExpressionOpening": 1,
|
||||
"AssignmentOperator": 1,
|
||||
"BinaryExpression": 0,
|
||||
"BinaryExpressionOperator": 1,
|
||||
"BlockComment": 1,
|
||||
"CallExpression": 1,
|
||||
"CatchClosingBrace": 1,
|
||||
"CatchKeyword": 1,
|
||||
"CatchOpeningBrace": 1,
|
||||
"CatchParameterList": 0,
|
||||
"CommaOperator": 0,
|
||||
"ConditionalExpressionAlternate": 1,
|
||||
"ConditionalExpressionConsequent": 1,
|
||||
"DoWhileStatementClosingBrace": 1,
|
||||
"DoWhileStatementConditional": 1,
|
||||
"DoWhileStatementOpeningBrace": 1,
|
||||
"ElseIfStatementClosingBrace": 1,
|
||||
"ElseIfStatementOpeningBrace": 1,
|
||||
"ElseStatementClosingBrace": 1,
|
||||
"ElseStatementOpeningBrace": 1,
|
||||
"EmptyStatement": 0,
|
||||
"ExpressionClosingParentheses": 0,
|
||||
"FinallyClosingBrace": 1,
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": 1,
|
||||
"ForInStatement": 1,
|
||||
"ForInStatementClosingBrace": 1,
|
||||
"ForInStatementExpressionClosing": 0,
|
||||
"ForInStatementExpressionOpening": 1,
|
||||
"ForInStatementOpeningBrace": 1,
|
||||
"ForStatement": 1,
|
||||
"ForStatementClosingBrace": 1,
|
||||
"ForStatementExpressionClosing": 0,
|
||||
"ForStatementExpressionOpening": 1,
|
||||
"ForStatementOpeningBrace": 1,
|
||||
"ForStatementSemicolon": 0,
|
||||
"FunctionDeclarationClosingBrace": 1,
|
||||
"FunctionDeclarationOpeningBrace": 1,
|
||||
"FunctionExpressionClosingBrace": 1,
|
||||
"FunctionExpressionOpeningBrace": 1,
|
||||
"IfStatementClosingBrace": 1,
|
||||
"IfStatementConditionalClosing": 0,
|
||||
"IfStatementConditionalOpening": 1,
|
||||
"IfStatementOpeningBrace": 1,
|
||||
"LineComment": 1,
|
||||
"LogicalExpressionOperator": 1,
|
||||
"MemberExpressionClosing": 0,
|
||||
"ObjectExpressionClosingBrace": 1,
|
||||
"ParameterComma": 0,
|
||||
"ParameterList": 0,
|
||||
"Property": 1,
|
||||
"PropertyName": 1,
|
||||
"PropertyValue": 1,
|
||||
"SwitchDiscriminantClosing": 0,
|
||||
"SwitchDiscriminantOpening": 1,
|
||||
"ThrowKeyword": 1,
|
||||
"TryClosingBrace": 1,
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": 1,
|
||||
"UnaryExpressionOperator": 0,
|
||||
"VariableName": 1,
|
||||
"VariableValue": 1,
|
||||
"WhileStatementClosingBrace": 1,
|
||||
"WhileStatementConditionalClosing": 0,
|
||||
"WhileStatementConditionalOpening": 1,
|
||||
"WhileStatementOpeningBrace": 1
|
||||
},
|
||||
"after": {
|
||||
"ArgumentComma": 1,
|
||||
"ArgumentList": 0,
|
||||
"ArgumentListArrayExpression": 1,
|
||||
"ArgumentListFunctionExpression": 1,
|
||||
"ArgumentListObjectExpression": 0,
|
||||
"ArrayExpressionClosing": 0,
|
||||
"ArrayExpressionComma": 1,
|
||||
"ArrayExpressionOpening": 0,
|
||||
"AssignmentOperator": 1,
|
||||
"BinaryExpression": 0,
|
||||
"BinaryExpressionOperator": 1,
|
||||
"BlockComment": 1,
|
||||
"CallExpression": 0,
|
||||
"CatchClosingBrace": 1,
|
||||
"CatchKeyword": 1,
|
||||
"CatchOpeningBrace": 1,
|
||||
"CatchParameterList": 0,
|
||||
"CommaOperator": 1,
|
||||
"ConditionalExpressionConsequent": 1,
|
||||
"ConditionalExpressionTest": 1,
|
||||
"DoWhileStatementBody": 1,
|
||||
"DoWhileStatementClosingBrace": 1,
|
||||
"DoWhileStatementOpeningBrace": 1,
|
||||
"ElseIfStatementClosingBrace": 1,
|
||||
"ElseIfStatementOpeningBrace": 1,
|
||||
"ElseStatementClosingBrace": 1,
|
||||
"ElseStatementOpeningBrace": 1,
|
||||
"EmptyStatement": 0,
|
||||
"ExpressionOpeningParentheses": 0,
|
||||
"FinallyClosingBrace": 1,
|
||||
"FinallyKeyword": -1,
|
||||
"FinallyOpeningBrace": 1,
|
||||
"ForInStatement": 1,
|
||||
"ForInStatementClosingBrace": 1,
|
||||
"ForInStatementExpressionClosing": 1,
|
||||
"ForInStatementExpressionOpening": 0,
|
||||
"ForInStatementOpeningBrace": 1,
|
||||
"ForStatement": 1,
|
||||
"ForStatementClosingBrace": 1,
|
||||
"ForStatementExpressionClosing": 1,
|
||||
"ForStatementExpressionOpening": 0,
|
||||
"ForStatementOpeningBrace": 1,
|
||||
"ForStatementSemicolon": 1,
|
||||
"FunctionDeclarationClosingBrace": 0,
|
||||
"FunctionDeclarationOpeningBrace": 0,
|
||||
"FunctionExpressionClosingBrace": 0,
|
||||
"FunctionExpressionOpeningBrace": 0,
|
||||
"FunctionName": 0,
|
||||
"FunctionReservedWord": 0,
|
||||
"IfStatementClosingBrace": 1,
|
||||
"IfStatementConditionalClosing": 0,
|
||||
"IfStatementConditionalOpening": 0,
|
||||
"IfStatementOpeningBrace": 1,
|
||||
"LogicalExpressionOperator": 1,
|
||||
"MemberExpressionOpening": 0,
|
||||
"ObjectExpressionClosingBrace": 0,
|
||||
"ObjectExpressionOpeningBrace": 1,
|
||||
"ParameterComma": 1,
|
||||
"ParameterList": 0,
|
||||
"PropertyName": 0,
|
||||
"PropertyValue": 0,
|
||||
"SwitchDiscriminantClosing": 1,
|
||||
"SwitchDiscriminantOpening": 0,
|
||||
"ThrowKeyword": 1,
|
||||
"TryClosingBrace": 1,
|
||||
"TryKeyword": -1,
|
||||
"TryOpeningBrace": 1,
|
||||
"UnaryExpressionOperator": 0,
|
||||
"VariableName": 1,
|
||||
"WhileStatementClosingBrace": 1,
|
||||
"WhileStatementConditionalClosing": 1,
|
||||
"WhileStatementConditionalOpening": 0,
|
||||
"WhileStatementOpeningBrace": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
**/JsLibraries/**
|
||||
@@ -1,327 +0,0 @@
|
||||
const fs = require('fs');
|
||||
|
||||
const dirs = fs
|
||||
.readdirSync('frontend/src', { withFileTypes: true })
|
||||
.filter((dirent) => dirent.isDirectory())
|
||||
.map((dirent) => dirent.name)
|
||||
.join('|');
|
||||
|
||||
module.exports = {
|
||||
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',
|
||||
'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/sort': '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/sort': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
// Packages
|
||||
// Absolute Paths
|
||||
// Relative Paths
|
||||
// Css
|
||||
['^@?\\w', `^(${dirs})(/.*|$)`, '^\\.', '^\\..*css$']
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"js": {
|
||||
"indent_size": 2,
|
||||
"indent_char": " ",
|
||||
"indent_level": 2,
|
||||
"indent_with_tabs": false,
|
||||
"preserve_newlines": true,
|
||||
"brace_style": "collapse",
|
||||
"max_preserve_newlines": 2,
|
||||
"jslint_happy": true
|
||||
}
|
||||
}
|
||||
@@ -1,396 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"stylelint-order"
|
||||
],
|
||||
"ignoreFiles": [
|
||||
"frontend/src/Styles/scaffolding.css",
|
||||
"**/*.js"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": [
|
||||
"inside-block"
|
||||
]
|
||||
}
|
||||
],
|
||||
"at-rule-name-case": "lower",
|
||||
"at-rule-name-newline-after": "always-multi-line",
|
||||
"at-rule-name-space-after": "always",
|
||||
"at-rule-no-unknown": [
|
||||
true,
|
||||
{
|
||||
"ignoreAtRules": [
|
||||
"/^add\\-mixin$/",
|
||||
"/^define\\-mixin$/"
|
||||
]
|
||||
}
|
||||
],
|
||||
"at-rule-no-vendor-prefix": true,
|
||||
"at-rule-semicolon-newline-after": "always",
|
||||
"at-rule-semicolon-space-before": "never",
|
||||
"block-closing-brace-empty-line-before": "never",
|
||||
"block-closing-brace-newline-after": "always",
|
||||
"block-closing-brace-newline-before": "always",
|
||||
"block-closing-brace-space-after": "always-single-line",
|
||||
"block-closing-brace-space-before": "always-single-line",
|
||||
"block-no-empty": true,
|
||||
"block-opening-brace-newline-after": "always",
|
||||
"block-opening-brace-newline-before": "never-single-line",
|
||||
"block-opening-brace-space-after": "always-single-line",
|
||||
"block-opening-brace-space-before": "always",
|
||||
"color-hex-case": "lower",
|
||||
"color-hex-length": "short",
|
||||
"color-named": "never",
|
||||
"color-no-invalid-hex": true,
|
||||
"comment-whitespace-inside": "always",
|
||||
"declaration-bang-space-after": "never",
|
||||
"declaration-bang-space-before": "always",
|
||||
"declaration-block-no-duplicate-properties": [
|
||||
true,
|
||||
{
|
||||
"ignoreProperties": [
|
||||
"composes"
|
||||
]
|
||||
}
|
||||
],
|
||||
"declaration-block-no-redundant-longhand-properties": true,
|
||||
"declaration-block-no-shorthand-property-overrides": true,
|
||||
"declaration-block-semicolon-newline-after": "always",
|
||||
"declaration-block-semicolon-newline-before": "never-multi-line",
|
||||
"declaration-block-semicolon-space-before": "never",
|
||||
"declaration-block-single-line-max-declarations": 1,
|
||||
"declaration-block-trailing-semicolon": "always",
|
||||
"declaration-colon-space-after": "always",
|
||||
"declaration-colon-space-before": "never",
|
||||
"font-family-name-quotes": "always-unless-keyword",
|
||||
"function-calc-no-unspaced-operator": true,
|
||||
"function-comma-newline-after": "never-multi-line",
|
||||
"function-comma-newline-before": "never-multi-line",
|
||||
"function-comma-space-after": "always",
|
||||
"function-comma-space-before": "never",
|
||||
"function-linear-gradient-no-nonstandard-direction": true,
|
||||
"function-name-case": "lower",
|
||||
"function-parentheses-newline-inside": "never-multi-line",
|
||||
"function-parentheses-space-inside": "never",
|
||||
"function-url-quotes": "always",
|
||||
"function-url-scheme-blacklist": [
|
||||
"data"
|
||||
],
|
||||
"function-whitespace-after": "always",
|
||||
"indentation": 2,
|
||||
"keyframe-declaration-no-important": true,
|
||||
"length-zero-no-unit": true,
|
||||
"max-empty-lines": 1,
|
||||
"max-line-length": [
|
||||
100,
|
||||
{
|
||||
"ignore": [
|
||||
"non-comments"
|
||||
]
|
||||
}
|
||||
],
|
||||
"max-nesting-depth": 2,
|
||||
"media-feature-colon-space-after": "always",
|
||||
"media-feature-colon-space-before": "never",
|
||||
"media-feature-name-case": "lower",
|
||||
"media-feature-name-no-vendor-prefix": true,
|
||||
"media-feature-range-operator-space-after": "always",
|
||||
"media-feature-range-operator-space-before": "always",
|
||||
"no-empty-source": true,
|
||||
"no-eol-whitespace": true,
|
||||
"no-extra-semicolons": true,
|
||||
"no-invalid-double-slash-comments": true,
|
||||
"no-missing-end-of-source-newline": true,
|
||||
"number-leading-zero": "always",
|
||||
"number-no-trailing-zeros": true,
|
||||
"order/order": [
|
||||
"custom-properties",
|
||||
"dollar-variables",
|
||||
{
|
||||
"hasBlock": false,
|
||||
"name": "add-mixin",
|
||||
"type": "at-rule"
|
||||
},
|
||||
"declarations",
|
||||
"rules",
|
||||
"at-rules"
|
||||
],
|
||||
"order/properties-order": [
|
||||
{
|
||||
"emptyLineBefore": "always",
|
||||
"properties": [
|
||||
"composes"
|
||||
]
|
||||
},
|
||||
{
|
||||
"emptyLineBefore": "always",
|
||||
"properties": [
|
||||
"position",
|
||||
"top",
|
||||
"right",
|
||||
"bottom",
|
||||
"left",
|
||||
"z-index",
|
||||
"display",
|
||||
"visibility",
|
||||
"align-content",
|
||||
"align-items",
|
||||
"align-self",
|
||||
"justify-content",
|
||||
"flex",
|
||||
"flex-direction",
|
||||
"flex-order",
|
||||
"flex-pack",
|
||||
"flex-align",
|
||||
"flex-grow",
|
||||
"flex-shrink",
|
||||
"flex-basis",
|
||||
"flex-wrap",
|
||||
"flex-flow",
|
||||
"float",
|
||||
"clear",
|
||||
"overflow",
|
||||
"overflow-x",
|
||||
"overflow-y",
|
||||
"-webkit-overflow-scrolling",
|
||||
"clip",
|
||||
"box-sizing",
|
||||
"margin",
|
||||
"margin-top",
|
||||
"margin-right",
|
||||
"margin-bottom",
|
||||
"margin-left",
|
||||
"padding",
|
||||
"padding-top",
|
||||
"padding-right",
|
||||
"padding-bottom",
|
||||
"padding-left",
|
||||
"min-width",
|
||||
"min-height",
|
||||
"max-width",
|
||||
"max-height",
|
||||
"width",
|
||||
"height",
|
||||
"outline",
|
||||
"outline-width",
|
||||
"outline-style",
|
||||
"outline-color",
|
||||
"outline-offset",
|
||||
"border",
|
||||
"border-spacing",
|
||||
"border-collapse",
|
||||
"border-width",
|
||||
"border-style",
|
||||
"border-color",
|
||||
"border-top",
|
||||
"border-top-width",
|
||||
"border-top-style",
|
||||
"border-top-color",
|
||||
"border-right",
|
||||
"border-right-width",
|
||||
"border-right-style",
|
||||
"border-right-color",
|
||||
"border-bottom",
|
||||
"border-bottom-width",
|
||||
"border-bottom-style",
|
||||
"border-bottom-color",
|
||||
"border-left",
|
||||
"border-left-width",
|
||||
"border-left-style",
|
||||
"border-left-color",
|
||||
"border-radius",
|
||||
"border-top-left-radius",
|
||||
"border-top-right-radius",
|
||||
"border-bottom-right-radius",
|
||||
"border-bottom-left-radius",
|
||||
"border-image",
|
||||
"border-image-source",
|
||||
"border-image-slice",
|
||||
"border-image-width",
|
||||
"border-image-outset",
|
||||
"border-image-repeat",
|
||||
"border-top-image",
|
||||
"border-right-image",
|
||||
"border-bottom-image",
|
||||
"border-left-image",
|
||||
"border-corner-image",
|
||||
"border-top-left-image",
|
||||
"border-top-right-image",
|
||||
"border-bottom-right-image",
|
||||
"border-bottom-left-image",
|
||||
"background",
|
||||
"background-color",
|
||||
"background-image",
|
||||
"background-attachment",
|
||||
"background-position",
|
||||
"background-position-x",
|
||||
"background-position-y",
|
||||
"background-clip",
|
||||
"background-origin",
|
||||
"background-size",
|
||||
"background-repeat",
|
||||
"box-decoration-break",
|
||||
"box-shadow",
|
||||
"color",
|
||||
"table-layout",
|
||||
"caption-side",
|
||||
"empty-cells",
|
||||
"list-style",
|
||||
"list-style-position",
|
||||
"list-style-type",
|
||||
"list-style-image",
|
||||
"quotes",
|
||||
"content",
|
||||
"counter-increment",
|
||||
"counter-reset",
|
||||
"-ms-writing-mode",
|
||||
"vertical-align",
|
||||
"text-align",
|
||||
"text-align-last",
|
||||
"text-decoration",
|
||||
"text-emphasis",
|
||||
"text-emphasis-position",
|
||||
"text-emphasis-style",
|
||||
"text-emphasis-color",
|
||||
"text-indent",
|
||||
"text-justify",
|
||||
"text-outline",
|
||||
"text-transform",
|
||||
"text-wrap",
|
||||
"text-overflow",
|
||||
"text-overflow-ellipsis",
|
||||
"text-overflow-mode",
|
||||
"text-shadow",
|
||||
"white-space",
|
||||
"word-spacing",
|
||||
"word-wrap",
|
||||
"word-break",
|
||||
"tab-size",
|
||||
"hyphens",
|
||||
"letter-spacing",
|
||||
"font",
|
||||
"font-weight",
|
||||
"font-style",
|
||||
"font-variant",
|
||||
"font-size-adjust",
|
||||
"font-stretch",
|
||||
"font-size",
|
||||
"font-family",
|
||||
"font-smoothing",
|
||||
"-moz-osx-font-smoothing",
|
||||
"-webkit-font-smoothing",
|
||||
"src",
|
||||
"line-height",
|
||||
"opacity",
|
||||
"filter",
|
||||
"resize",
|
||||
"cursor",
|
||||
"appearance",
|
||||
"nav-index",
|
||||
"nav-up",
|
||||
"nav-right",
|
||||
"nav-down",
|
||||
"nav-left",
|
||||
"transition",
|
||||
"transition-delay",
|
||||
"transition-timing-function",
|
||||
"transition-duration",
|
||||
"transition-property",
|
||||
"transform",
|
||||
"transform-origin",
|
||||
"transform-style",
|
||||
"backface-visibility",
|
||||
"animation",
|
||||
"animation-name",
|
||||
"animation-duration",
|
||||
"animation-play-state",
|
||||
"animation-timing-function",
|
||||
"animation-delay",
|
||||
"animation-iteration-count",
|
||||
"animation-direction",
|
||||
"animation-fill-mode",
|
||||
"pointer-events",
|
||||
"user-select",
|
||||
"touch-action",
|
||||
"-webkit-tap-highlight-color",
|
||||
"unicode-bidi",
|
||||
"direction",
|
||||
"columns",
|
||||
"column-span",
|
||||
"column-width",
|
||||
"column-count",
|
||||
"column-fill",
|
||||
"column-gap",
|
||||
"column-rule",
|
||||
"column-rule-width",
|
||||
"column-rule-style",
|
||||
"column-rule-color",
|
||||
"break-before",
|
||||
"break-inside",
|
||||
"break-after",
|
||||
"page-break-before",
|
||||
"page-break-inside",
|
||||
"page-break-after",
|
||||
"orphans",
|
||||
"widows",
|
||||
"zoom",
|
||||
"max-zoom",
|
||||
"min-zoom",
|
||||
"user-zoom",
|
||||
"orientation"
|
||||
]
|
||||
}
|
||||
],
|
||||
"property-case": "lower",
|
||||
"property-no-vendor-prefix": true,
|
||||
"rule-empty-line-before": [
|
||||
"always",
|
||||
{
|
||||
"except": [
|
||||
"first-nested"
|
||||
],
|
||||
"ignore": [
|
||||
"after-comment"
|
||||
]
|
||||
}
|
||||
],
|
||||
"selector-attribute-brackets-space-inside": "never",
|
||||
"selector-attribute-operator-space-after": "never",
|
||||
"selector-attribute-operator-space-before": "never",
|
||||
"selector-attribute-quotes": "never",
|
||||
"selector-class-pattern": "^[A-Za-z0-9]+$",
|
||||
"selector-combinator-space-after": "always",
|
||||
"selector-combinator-space-before": "always",
|
||||
"selector-descendant-combinator-no-non-space": true,
|
||||
"selector-list-comma-newline-after": "always",
|
||||
"selector-list-comma-newline-before": "never-multi-line",
|
||||
"selector-list-comma-space-before": "never",
|
||||
"selector-max-attribute": 0,
|
||||
"selector-max-class": 3,
|
||||
"selector-max-compound-selectors": 3,
|
||||
"selector-max-empty-lines": 0,
|
||||
"selector-max-id": 0,
|
||||
"selector-max-universal": 0,
|
||||
"selector-pseudo-class-case": "lower",
|
||||
"selector-pseudo-class-parentheses-space-inside": "never",
|
||||
"selector-pseudo-element-case": "lower",
|
||||
"selector-pseudo-element-colon-notation": "double",
|
||||
"selector-pseudo-element-no-unknown": true,
|
||||
"selector-type-case": "lower",
|
||||
"selector-type-no-unknown": true,
|
||||
"shorthand-property-no-redundant-values": true,
|
||||
"string-no-newline": true,
|
||||
"string-quotes": "single",
|
||||
"time-min-milliseconds": 100,
|
||||
"unit-case": "lower",
|
||||
"unit-no-unknown": true,
|
||||
"value-list-comma-newline-after": "never-multi-line",
|
||||
"value-list-comma-newline-before": "never-multi-line",
|
||||
"value-list-comma-space-after": "always",
|
||||
"value-list-comma-space-before": "never",
|
||||
"value-list-max-empty-lines": 0,
|
||||
"value-no-vendor-prefix": true
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"ecmaVersion": 6,
|
||||
"libs": [
|
||||
"browser",
|
||||
"jquery"
|
||||
]
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-proposal-optional-chaining', { loose }],
|
||||
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
|
||||
|
||||
// Stage 2
|
||||
'@babel/plugin-proposal-export-namespace-from',
|
||||
|
||||
// Stage 3
|
||||
['@babel/plugin-proposal-class-properties', { loose }],
|
||||
'@babel/plugin-syntax-dynamic-import'
|
||||
],
|
||||
env: {
|
||||
development: {
|
||||
presets: [
|
||||
['@babel/preset-react', { development: true }]
|
||||
],
|
||||
plugins: [
|
||||
'babel-plugin-inline-classnames'
|
||||
]
|
||||
},
|
||||
production: {
|
||||
presets: [
|
||||
'@babel/preset-react'
|
||||
],
|
||||
plugins: [
|
||||
'babel-plugin-transform-react-remove-prop-types'
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
|
||||
require('./clean');
|
||||
require('./copy');
|
||||
require('./webpack');
|
||||
|
||||
gulp.task('build',
|
||||
gulp.series('clean',
|
||||
gulp.parallel(
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
const gulp = require('gulp');
|
||||
const del = require('del');
|
||||
|
||||
const paths = require('./helpers/paths');
|
||||
|
||||
gulp.task('clean', () => {
|
||||
return del([paths.dest.root]);
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
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('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());
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
require('./build.js');
|
||||
require('./clean.js');
|
||||
require('./copy.js');
|
||||
require('./watch.js');
|
||||
require('./webpack.js');
|
||||
@@ -1,6 +0,0 @@
|
||||
const colors = require('ansi-colors');
|
||||
|
||||
module.exports = function errorHandler(error) {
|
||||
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
|
||||
this.emit('end');
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
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;
|
||||
@@ -1,18 +0,0 @@
|
||||
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));
|
||||
@@ -1,266 +0,0 @@
|
||||
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 OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
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 inlineWebWorkers = true;
|
||||
|
||||
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 OptimizeCssAssetsPlugin({}),
|
||||
|
||||
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',
|
||||
inline: inlineWebWorkers,
|
||||
fallback: !inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
configFile: `${frontendFolder}/babel.config.js`,
|
||||
envName: isProduction ? 'production' : 'development',
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
modules: false,
|
||||
loose: true,
|
||||
debug: false,
|
||||
useBuiltIns: 'entry',
|
||||
corejs: 3
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
// 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, webpack)
|
||||
.on('error', errorHandler)
|
||||
.pipe(gulp.dest('_output/UI'))
|
||||
.on('error', errorHandler)
|
||||
.pipe(livereload())
|
||||
.on('error', errorHandler);
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
const loaderUtils = require('loader-utils');
|
||||
|
||||
module.exports = function cssVariablesLoader(source) {
|
||||
const options = loaderUtils.getOptions(this);
|
||||
|
||||
options.cssVarsFiles.forEach((cssVarsFile) => {
|
||||
this.addDependency(cssVarsFile);
|
||||
});
|
||||
|
||||
return source;
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"checkJs": false,
|
||||
"baseUrl": "src",
|
||||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"*": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
const reload = require('require-nocache')(module);
|
||||
|
||||
module.exports = (ctx, configPath, options) => {
|
||||
const config = {
|
||||
plugins: {
|
||||
'postcss-mixins': {
|
||||
mixinsDir: [
|
||||
'frontend/src/Styles/Mixins'
|
||||
]
|
||||
},
|
||||
'postcss-simple-vars': {
|
||||
variables: () =>
|
||||
ctx.options.cssVarsFiles.reduce((acc, vars) => {
|
||||
return Object.assign(acc, reload(vars));
|
||||
}, {})
|
||||
},
|
||||
'postcss-color-function': {},
|
||||
'postcss-nested': {}
|
||||
}
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
4
frontend/src/.vscode/settings.json
vendored
@@ -1,4 +0,0 @@
|
||||
// Place your settings in this file to overwrite default and user settings.
|
||||
{
|
||||
"files.insertFinalNewline": true
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import 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 } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
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={translate('Blacklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={isClearingBlacklistExecuting}
|
||||
onPress={onClearBlacklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToLoadBlacklist')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</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;
|
||||
@@ -1,154 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Blacklist from './Blacklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blacklist,
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
|
||||
(blacklist, isClearingBlacklistExecuting) => {
|
||||
return {
|
||||
isClearingBlacklistExecuting,
|
||||
...blacklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blacklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlacklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchBlacklist,
|
||||
gotoBlacklistFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchBlacklist();
|
||||
} else {
|
||||
gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearBlacklist();
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlacklist();
|
||||
}
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlacklistPreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlacklistNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlacklistLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
}
|
||||
|
||||
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)
|
||||
);
|
||||
@@ -1,90 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class BlacklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Details
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Protocol')}
|
||||
data={protocol}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlacklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlacklistDetailsModal;
|
||||
@@ -1,18 +0,0 @@
|
||||
.language,
|
||||
.quality {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
import styles from './BlacklistRow.css';
|
||||
|
||||
class BlacklistRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
movie,
|
||||
sourceTitle,
|
||||
quality,
|
||||
customFormats,
|
||||
languages,
|
||||
date,
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
columns,
|
||||
onRemovePress
|
||||
} = this.props;
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sourceTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.quality}
|
||||
>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlacklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRemovePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<BlacklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
indexer={indexer}
|
||||
message={message}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
BlacklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movie: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlacklistRow;
|
||||
@@ -1,26 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
(movie) => {
|
||||
return {
|
||||
movie
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeFromBlacklist({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
|
||||
@@ -1,5 +0,0 @@
|
||||
.description {
|
||||
composes: description from '~Components/DescriptionList/DescriptionListItemDescription.css';
|
||||
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
@@ -1,279 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
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 formatAge from 'Utilities/Number/formatAge';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
function HistoryDetails(props) {
|
||||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
data,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (eventType === 'grabbed') {
|
||||
const {
|
||||
indexer,
|
||||
releaseGroup,
|
||||
nzbInfoUrl,
|
||||
downloadClient,
|
||||
downloadId,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishedDate
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title={translate('Indexer')}
|
||||
data={indexer}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('ReleaseGroup')}
|
||||
data={releaseGroup}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!nzbInfoUrl &&
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
Info URL
|
||||
</DescriptionListItemTitle>
|
||||
|
||||
<DescriptionListItemDescription>
|
||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||
</DescriptionListItemDescription>
|
||||
</span>
|
||||
}
|
||||
|
||||
{
|
||||
!!downloadClient &&
|
||||
<DescriptionListItem
|
||||
title={translate('DownloadClient')}
|
||||
data={downloadClient}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!downloadId &&
|
||||
<DescriptionListItem
|
||||
title={translate('GrabID')}
|
||||
data={downloadId}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!indexer &&
|
||||
<DescriptionListItem
|
||||
title={translate('AgeWhenGrabbed')}
|
||||
data={formatAge(age, ageHours, ageMinutes)}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!publishedDate &&
|
||||
<DescriptionListItem
|
||||
title={translate('PublishedDate')}
|
||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadFailed') {
|
||||
const {
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadFolderImported') {
|
||||
const {
|
||||
droppedPath,
|
||||
importedPath
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!droppedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Source')}
|
||||
data={droppedPath}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
!!importedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('ImportedTo')}
|
||||
data={importedPath}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'movieFileDeleted') {
|
||||
const {
|
||||
reason
|
||||
} = data;
|
||||
|
||||
let reasonMessage = '';
|
||||
|
||||
switch (reason) {
|
||||
case 'Manual':
|
||||
reasonMessage = 'File was deleted by via UI';
|
||||
break;
|
||||
case 'MissingFromDisk':
|
||||
reasonMessage = 'Radarr was unable to find the file on disk so it was removed';
|
||||
break;
|
||||
case 'Upgrade':
|
||||
reasonMessage = 'File was deleted to import an upgrade';
|
||||
break;
|
||||
default:
|
||||
reasonMessage = '';
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Reason')}
|
||||
data={reasonMessage}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'movieFileRenamed') {
|
||||
const {
|
||||
sourcePath,
|
||||
sourceRelativePath,
|
||||
path,
|
||||
relativePath
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('SourcePath')}
|
||||
data={sourcePath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('SourceRelativePath')}
|
||||
data={sourceRelativePath}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DestinationPath')}
|
||||
data={path}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DestinationRelativePath')}
|
||||
data={relativePath}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadIgnored') {
|
||||
const {
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title={translate('Message')}
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('Name')}
|
||||
data={sourceTitle}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
HistoryDetails.propTypes = {
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default HistoryDetails;
|
||||
@@ -1,19 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import HistoryDetails from './HistoryDetails';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return _.pick(uiSettings, [
|
||||
'shortDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(HistoryDetails);
|
||||
@@ -1,5 +0,0 @@
|
||||
.markAsFailedButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryDetails from './HistoryDetails';
|
||||
import styles from './HistoryDetailsModal.css';
|
||||
|
||||
function getHeaderTitle(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return 'Grabbed';
|
||||
case 'downloadFailed':
|
||||
return 'Download Failed';
|
||||
case 'downloadFolderImported':
|
||||
return 'Movie Imported';
|
||||
case 'movieFileDeleted':
|
||||
return 'Movie File Deleted';
|
||||
case 'movieFileRenamed':
|
||||
return 'Movie File Renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Download Ignored';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
function HistoryDetailsModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
data,
|
||||
isMarkingAsFailed,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{getHeaderTitle(eventType)}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<HistoryDetails
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<SpinnerButton
|
||||
className={styles.markAsFailedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isMarkingAsFailed}
|
||||
onPress={onMarkAsFailedPress}
|
||||
>
|
||||
Mark as Failed
|
||||
</SpinnerButton>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
HistoryDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
HistoryDetailsModal.defaultProps = {
|
||||
isMarkingAsFailed: false
|
||||
};
|
||||
|
||||
export default HistoryDetailsModal;
|
||||
@@ -1,153 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
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 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 translate from 'Utilities/String/translate';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
class History extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isMoviesFetching,
|
||||
isMoviesPopulated,
|
||||
moviesError,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
totalRecords,
|
||||
onFilterSelect,
|
||||
onFirstPagePress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const isFetchingAny = isFetching || isMoviesFetching;
|
||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
|
||||
const hasError = error || moviesError;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('History')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onFirstPagePress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isFetchingAny && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetchingAny && hasError &&
|
||||
<div>
|
||||
{translate('UnableToLoadHistory')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
// If history isPopulated and it's empty show no history found and don't
|
||||
// wait for the episodes to populate because they are never coming.
|
||||
|
||||
isPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
No history found
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<HistoryRowConnector
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isFetching}
|
||||
onFirstPagePress={onFirstPagePress}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
History.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isMoviesFetching: PropTypes.bool.isRequired,
|
||||
isMoviesPopulated: PropTypes.bool.isRequired,
|
||||
moviesError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onFirstPagePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default History;
|
||||
@@ -1,138 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.movies,
|
||||
(history, movies) => {
|
||||
return {
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
...history
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...historyActions
|
||||
};
|
||||
|
||||
class HistoryConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchHistory,
|
||||
gotoHistoryFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchHistory();
|
||||
} else {
|
||||
gotoHistoryFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoHistoryPreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoHistoryNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoHistoryLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoHistoryPage({ page });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setHistorySort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setHistoryFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setHistoryTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoHistoryFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<History
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
HistoryConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchHistory: PropTypes.func.isRequired,
|
||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
||||
gotoHistoryPreviousPage: PropTypes.func.isRequired,
|
||||
gotoHistoryNextPage: PropTypes.func.isRequired,
|
||||
gotoHistoryLastPage: PropTypes.func.isRequired,
|
||||
gotoHistoryPage: PropTypes.func.isRequired,
|
||||
setHistorySort: PropTypes.func.isRequired,
|
||||
setHistoryFilter: PropTypes.func.isRequired,
|
||||
setHistoryTableOption: PropTypes.func.isRequired,
|
||||
clearHistory: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
|
||||
);
|
||||
@@ -1,6 +0,0 @@
|
||||
.cell {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 35px;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import styles from './HistoryEventTypeCell.css';
|
||||
|
||||
function getIconName(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'movieFolderImported':
|
||||
return icons.DRIVE;
|
||||
case 'downloadFolderImported':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadFailed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'movieFileDeleted':
|
||||
return icons.DELETE;
|
||||
case 'movieFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
return icons.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
function getIconKind(eventType) {
|
||||
switch (eventType) {
|
||||
case 'downloadFailed':
|
||||
return kinds.DANGER;
|
||||
default:
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
function getTooltip(eventType, data) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return `Movie grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
|
||||
case 'movieFolderImported':
|
||||
return 'Movie imported from movie folder';
|
||||
case 'downloadFolderImported':
|
||||
return 'Movie downloaded successfully and picked up from download client';
|
||||
case 'downloadFailed':
|
||||
return 'Movie download failed';
|
||||
case 'movieFileDeleted':
|
||||
return 'Movie file deleted';
|
||||
case 'movieFileRenamed':
|
||||
return 'Movie file renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Movie Download Ignored';
|
||||
default:
|
||||
return 'Unknown event';
|
||||
}
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, data }) {
|
||||
const iconName = getIconName(eventType);
|
||||
const iconKind = getIconKind(eventType);
|
||||
const tooltip = getTooltip(eventType, data);
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.cell}
|
||||
title={tooltip}
|
||||
>
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
HistoryEventTypeCell.propTypes = {
|
||||
eventType: PropTypes.string.isRequired,
|
||||
data: PropTypes.object
|
||||
};
|
||||
|
||||
HistoryEventTypeCell.defaultProps = {
|
||||
data: {}
|
||||
};
|
||||
|
||||
export default HistoryEventTypeCell;
|
||||
@@ -1,23 +0,0 @@
|
||||
.downloadClient {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 120px;
|
||||
}
|
||||
|
||||
.indexer {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
@@ -1,236 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
import styles from './HistoryRow.css';
|
||||
|
||||
class HistoryRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isDetailsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.isMarkingAsFailed &&
|
||||
!this.props.isMarkingAsFailed &&
|
||||
!this.props.markAsFailedError
|
||||
) {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDetailsPress = () => {
|
||||
this.setState({ isDetailsModalOpen: true });
|
||||
}
|
||||
|
||||
onDetailsModalClose = () => {
|
||||
this.setState({ isDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
movie,
|
||||
quality,
|
||||
customFormats,
|
||||
languages,
|
||||
qualityCutoffNotMet,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
date,
|
||||
data,
|
||||
isMarkingAsFailed,
|
||||
columns,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
onMarkAsFailedPress
|
||||
} = this.props;
|
||||
|
||||
if (!movie) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'eventType') {
|
||||
return (
|
||||
<HistoryEventTypeCell
|
||||
key={name}
|
||||
eventType={eventType}
|
||||
data={data}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
isCutoffMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'date') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={date}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.downloadClient}
|
||||
>
|
||||
{data.downloadClient}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.indexer}
|
||||
>
|
||||
{data.indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseGroup') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.releaseGroup}
|
||||
>
|
||||
{data.releaseGroup}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'details') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.details}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.INFO}
|
||||
onPress={this.onDetailsPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<HistoryDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
isMarkingAsFailed={isMarkingAsFailed}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HistoryRow.propTypes = {
|
||||
movieId: PropTypes.number,
|
||||
movie: PropTypes.object.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
isMarkingAsFailed: PropTypes.bool,
|
||||
markAsFailedError: PropTypes.object,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default HistoryRow;
|
||||
@@ -1,73 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import HistoryRow from './HistoryRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
createUISettingsSelector(),
|
||||
(movie, uiSettings) => {
|
||||
return {
|
||||
movie,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchHistory,
|
||||
markAsFailed
|
||||
};
|
||||
|
||||
class HistoryRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
prevProps.isMarkingAsFailed &&
|
||||
!this.props.isMarkingAsFailed &&
|
||||
!this.props.markAsFailedError
|
||||
) {
|
||||
this.props.fetchHistory();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.props.markAsFailed({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<HistoryRow
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
HistoryRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
isMarkingAsFailed: PropTypes.bool,
|
||||
markAsFailedError: PropTypes.object,
|
||||
fetchHistory: PropTypes.func.isRequired,
|
||||
markAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);
|
||||
@@ -1,13 +0,0 @@
|
||||
.torrent {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: $torrentColor;
|
||||
background-color: $torrentColor;
|
||||
}
|
||||
|
||||
.usenet {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
border-color: $usenetColor;
|
||||
background-color: $usenetColor;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import styles from './ProtocolLabel.css';
|
||||
|
||||
function ProtocolLabel({ protocol }) {
|
||||
const protocolName = protocol === 'usenet' ? 'nzb' : protocol;
|
||||
|
||||
return (
|
||||
<Label className={styles[protocol]}>
|
||||
{protocolName}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
ProtocolLabel.propTypes = {
|
||||
protocol: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default ProtocolLabel;
|
||||
@@ -1,292 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
isMoviesFetching
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(!isMoviesFetching && prevProps.isMoviesFetching) ||
|
||||
(!isFetching && prevProps.isFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.movieId))
|
||||
) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
this.setState({ isPendingSelected });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onGrabSelectedPress = () => {
|
||||
this.props.onGrabSelectedPress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isMoviesFetching,
|
||||
isMoviesPopulated,
|
||||
moviesError,
|
||||
columns,
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected,
|
||||
items
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
|
||||
const hasError = error || moviesError;
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Queue')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isRefreshing}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('GrabSelected')}
|
||||
iconName={icons.DOWNLOAD}
|
||||
isDisabled={disableSelectedActions || !isPendingSelected}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={this.onGrabSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RemoveSelected')}
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={disableSelectedActions}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isRefreshing && hasError &&
|
||||
<div>
|
||||
Failed to load Queue
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
Queue is empty
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<QueueRowConnector
|
||||
key={item.id}
|
||||
movieId={item.movieId}
|
||||
isSelected={selectedState[item.id]}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<TablePager
|
||||
totalRecords={totalRecords}
|
||||
isFetching={isRefreshing}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Queue.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isMoviesFetching: PropTypes.bool.isRequired,
|
||||
isMoviesPopulated: PropTypes.bool.isRequired,
|
||||
moviesError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Queue;
|
||||
@@ -1,170 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movies,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...queueActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class QueueConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchQueue,
|
||||
gotoQueueFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchQueue();
|
||||
} else {
|
||||
gotoQueueFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (
|
||||
this.props.includeUnknownMovieItems !==
|
||||
prevProps.includeUnknownMovieItems
|
||||
) {
|
||||
this.repopulate();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearQueue();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchQueue();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoQueuePreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoQueueNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoQueueLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoQueuePage({ page });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setQueueSort({ sortKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setQueueTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoQueueFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
}
|
||||
|
||||
onGrabSelectedPress = (ids) => {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Queue
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||
onRemoveSelectedPress={this.onRemoveSelectedPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueConnector.propTypes = {
|
||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchQueue: PropTypes.func.isRequired,
|
||||
gotoQueueFirstPage: PropTypes.func.isRequired,
|
||||
gotoQueuePreviousPage: PropTypes.func.isRequired,
|
||||
gotoQueueNextPage: PropTypes.func.isRequired,
|
||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||
gotoQueuePage: PropTypes.func.isRequired,
|
||||
setQueueSort: PropTypes.func.isRequired,
|
||||
setQueueTableOption: PropTypes.func.isRequired,
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
removeQueueItems: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
||||
);
|
||||
@@ -1,98 +0,0 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function QueueDetails(props) {
|
||||
const {
|
||||
title,
|
||||
size,
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status: queueStatus,
|
||||
errorMessage,
|
||||
progressBar
|
||||
} = props;
|
||||
|
||||
const status = queueStatus.toLowerCase();
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
|
||||
if (status === 'pending') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.PENDING}
|
||||
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('ImportFailedInterp', [errorMessage])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: show an icon when download is complete, but not imported yet?
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DownloadFailedInterp', [errorMessage])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (progress < 5) {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return progressBar;
|
||||
}
|
||||
|
||||
QueueDetails.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
export default QueueDetails;
|
||||
@@ -1,78 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class QueueOptions extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
includeUnknownMovieItems: props.includeUnknownMovieItems
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
includeUnknownMovieItems
|
||||
} = this.props;
|
||||
|
||||
if (includeUnknownMovieItems !== prevProps.includeUnknownMovieItems) {
|
||||
this.setState({
|
||||
includeUnknownMovieItems
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOptionChange = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onOptionChange({
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
includeUnknownMovieItems
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowUnknownMovieItems')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownMovieItems"
|
||||
value={includeUnknownMovieItems}
|
||||
helpText={translate('IncludeUnknownMovieItemsHelpText')}
|
||||
onChange={this.onOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueOptions.propTypes = {
|
||||
includeUnknownMovieItems: PropTypes.bool.isRequired,
|
||||
onOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QueueOptions;
|
||||
@@ -1,19 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||
import QueueOptions from './QueueOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.queue.options,
|
||||
(options) => {
|
||||
return options;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onOptionChange: setQueueOption
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
||||
@@ -1,23 +0,0 @@
|
||||
.quality {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.protocol {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 70px;
|
||||
}
|
||||
@@ -1,370 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import QueueStatusCell from './QueueStatusCell';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
import TimeleftCell from './TimeleftCell';
|
||||
import styles from './QueueRow.css';
|
||||
|
||||
class QueueRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isRemoveQueueItemModalOpen: false,
|
||||
isInteractiveImportModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveQueueItemPress = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blacklist) => {
|
||||
this.props.onRemoveQueueItemPress(blacklist);
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalClose = () => {
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
downloadId,
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
movie,
|
||||
quality,
|
||||
customFormats,
|
||||
languages,
|
||||
protocol,
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
isRemoving,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isRemoveQueueItemModalOpen,
|
||||
isInteractiveImportModalOpen
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<QueueStatusCell
|
||||
key={name}
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'movies.sortTitle') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
movie ?
|
||||
<MovieTitleLink
|
||||
titleSlug={movie.titleSlug}
|
||||
title={movie.title}
|
||||
/> :
|
||||
title
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'languages') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
quality ?
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'customFormats') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'protocol') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'indexer') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'downloadClient') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{downloadClient}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'size') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'title') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{title}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'outputPath') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{outputPath}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'estimatedCompletionTime') {
|
||||
return (
|
||||
<TimeleftCell
|
||||
key={name}
|
||||
status={status}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
timeleft={timeleft}
|
||||
size={size}
|
||||
sizeleft={sizeleft}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'progress') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.progress}
|
||||
>
|
||||
{
|
||||
!!progress &&
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
title={`${progress.toFixed(1)}%`}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
key={name}
|
||||
className={styles.actions}
|
||||
>
|
||||
{
|
||||
showInteractiveImport &&
|
||||
<IconButton
|
||||
name={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isPending &&
|
||||
<SpinnerIconButton
|
||||
name={icons.DOWNLOAD}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<SpinnerIconButton
|
||||
title={translate('RemoveFromQueue')}
|
||||
name={icons.REMOVE}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
downloadId={downloadId}
|
||||
title={title}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!movie}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
QueueRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
downloadId: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadState: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
movie: PropTypes.object,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
sizeleft: PropTypes.number,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.object,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
onRemoveQueueItemPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
QueueRow.defaultProps = {
|
||||
isGrabbing: false,
|
||||
isRemoving: false
|
||||
};
|
||||
|
||||
export default QueueRow;
|
||||
@@ -1,67 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { grabQueueItem, removeQueueItem } from 'Store/Actions/queueActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import QueueRow from './QueueRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
createUISettingsSelector(),
|
||||
(movie, uiSettings) => {
|
||||
const result = {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
|
||||
result.movie = movie;
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
grabQueueItem,
|
||||
removeQueueItem
|
||||
};
|
||||
|
||||
class QueueRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<QueueRow
|
||||
{...this.props}
|
||||
onGrabPress={this.onGrabPress}
|
||||
onRemoveQueueItemPress={this.onRemoveQueueItemPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
movie: PropTypes.object,
|
||||
grabQueueItem: PropTypes.func.isRequired,
|
||||
removeQueueItem: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueRowConnector);
|
||||
@@ -1,5 +0,0 @@
|
||||
.status {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './QueueStatusCell.css';
|
||||
|
||||
function getDetailedPopoverBody(statusMessages) {
|
||||
return (
|
||||
<div>
|
||||
{
|
||||
statusMessages.map(({ title, messages }) => {
|
||||
return (
|
||||
<div key={title}>
|
||||
{title}
|
||||
<ul>
|
||||
{
|
||||
messages.map((message) => {
|
||||
return (
|
||||
<li key={message}>
|
||||
{message}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = translate('Downloading');
|
||||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = translate('Paused');
|
||||
}
|
||||
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = translate('Queued');
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = translate('Downloaded');
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ` - ${translate('WaitingToImport')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ` - ${translate('Importing')}`;
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ` - ${translate('WaitingToProcess')}`;
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = translate('Pending');
|
||||
}
|
||||
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `${translate('Pending')} - ${translate('DownloadClientUnavailable')}`;
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||
title = translate('DownloadWarning', [warningMessage]);
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('ImportFailed', [sourceTitle]);
|
||||
} else {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = translate('DownloadFailed');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowCell className={styles.status}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={iconName}
|
||||
kind={iconKind}
|
||||
/>
|
||||
}
|
||||
title={title}
|
||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||
position={tooltipPositions.RIGHT}
|
||||
canFlip={false}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: translate('Ok'),
|
||||
trackedDownloadState: translate('Downloading')
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
@@ -1,144 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove - {sourceTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlacklistRelease')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText={translate('BlacklistHelpText')}
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
@@ -1,3 +0,0 @@
|
||||
.message {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Remove Selected Item{selectedCount > 1 ? 's' : ''}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText={translate('BlacklistHelpText')}
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
||||
@@ -1,76 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import PageSidebarStatus from 'Components/Page/Sidebar/PageSidebarStatus';
|
||||
import { fetchQueueStatus } from 'Store/Actions/queueActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.app,
|
||||
(state) => state.queue.status,
|
||||
(state) => state.queue.options.includeUnknownMovieItems,
|
||||
(app, status, includeUnknownMovieItems) => {
|
||||
const {
|
||||
errors,
|
||||
warnings,
|
||||
unknownErrors,
|
||||
unknownWarnings,
|
||||
count,
|
||||
totalCount
|
||||
} = status.item;
|
||||
|
||||
return {
|
||||
isConnected: app.isConnected,
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: status.isPopulated,
|
||||
...status.item,
|
||||
count: includeUnknownMovieItems ? totalCount : count,
|
||||
errors: includeUnknownMovieItems ? errors || unknownErrors : errors,
|
||||
warnings: includeUnknownMovieItems ? warnings || unknownWarnings : warnings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchQueueStatus
|
||||
};
|
||||
|
||||
class QueueStatusConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.isConnected && prevProps.isReconnecting) {
|
||||
this.props.fetchQueueStatus();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageSidebarStatus
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueStatusConnector.propTypes = {
|
||||
isConnected: PropTypes.bool.isRequired,
|
||||
isReconnecting: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
fetchQueueStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueStatusConnector);
|
||||
@@ -1,5 +0,0 @@
|
||||
.timeleft {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 100px;
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import formatTime from 'Utilities/Date/formatTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './TimeleftCell.css';
|
||||
|
||||
function TimeleftCell(props) {
|
||||
const {
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
status,
|
||||
size,
|
||||
sizeleft,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (status === 'delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('DelayingDownloadUntilInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={translate('RetryingDownloadInterp', [date, time])}
|
||||
>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
const totalSize = formatBytes(size);
|
||||
const remainingSize = formatBytes(sizeleft);
|
||||
|
||||
return (
|
||||
<TableRowCell
|
||||
className={styles.timeleft}
|
||||
title={`${remainingSize} / ${totalSize}`}
|
||||
>
|
||||
{formatTimeSpan(timeleft)}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
TimeleftCell.propTypes = {
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default TimeleftCell;
|
||||
@@ -1,60 +0,0 @@
|
||||
.searchContainer {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.searchIconContainer {
|
||||
width: 58px;
|
||||
height: 46px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-right: none;
|
||||
border-radius: 4px;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
background-color: #edf1f2;
|
||||
text-align: center;
|
||||
line-height: 46px;
|
||||
}
|
||||
|
||||
.searchInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
height: 46px;
|
||||
border-radius: 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.clearLookupButton {
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-left: none;
|
||||
border-top-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.helpText {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.noMoviesText {
|
||||
margin-top: 80px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.noResults {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.searchResults {
|
||||
margin-top: 30px;
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddNewMovieSearchResultConnector from './AddNewMovieSearchResultConnector';
|
||||
import styles from './AddNewMovie.css';
|
||||
|
||||
class AddNewMovie extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
term: props.term || '',
|
||||
isFetching: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const term = this.state.term;
|
||||
|
||||
if (term) {
|
||||
this.props.onMovieLookupChange(term);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
term,
|
||||
isFetching
|
||||
} = this.props;
|
||||
|
||||
if (term && term !== prevProps.term) {
|
||||
this.setState({
|
||||
term,
|
||||
isFetching: true
|
||||
});
|
||||
this.props.onMovieLookupChange(term);
|
||||
} else if (isFetching !== prevProps.isFetching) {
|
||||
this.setState({
|
||||
isFetching
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchInputChange = ({ value }) => {
|
||||
const hasValue = !!value.trim();
|
||||
|
||||
this.setState({ term: value, isFetching: hasValue }, () => {
|
||||
if (hasValue) {
|
||||
this.props.onMovieLookupChange(value);
|
||||
} else {
|
||||
this.props.onClearMovieLookup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClearMovieLookupPress = () => {
|
||||
this.setState({ term: '' });
|
||||
this.props.onClearMovieLookup();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
error,
|
||||
items,
|
||||
hasExistingMovies
|
||||
} = this.props;
|
||||
|
||||
const term = this.state.term;
|
||||
const isFetching = this.state.isFetching;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('AddNewMovie')}>
|
||||
<PageContentBody>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon
|
||||
name={icons.SEARCH}
|
||||
size={20}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
name="movieLookup"
|
||||
value={term}
|
||||
placeholder="eg. The Dark Knight, tmdb:155, imdb:tt0468569"
|
||||
autoFocus={true}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className={styles.clearLookupButton}
|
||||
onPress={this.onClearMovieLookupPress}
|
||||
>
|
||||
<Icon
|
||||
name={icons.REMOVE}
|
||||
size={20}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
{translate('FailedLoadingSearchResults')}
|
||||
</div>
|
||||
<div>{getErrorMessage(error)}</div>
|
||||
</div> : null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error && !!items.length &&
|
||||
<div className={styles.searchResults}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<AddNewMovieSearchResultConnector
|
||||
key={item.tmdbId}
|
||||
{...item}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !error && !items.length && !!term &&
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noResults}>
|
||||
{translate('CouldNotFindResults', [term])}
|
||||
</div>
|
||||
<div>
|
||||
{translate('YouCanAlsoSearch')}
|
||||
</div>
|
||||
<div>
|
||||
<Link to="https://github.com/Radarr/Radarr/wiki/FAQ#why-cant-i-add-a-new-movie-when-i-know-the-tmdb-id">
|
||||
{translate('CantFindMovie')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
term ?
|
||||
null :
|
||||
<div className={styles.message}>
|
||||
<div className={styles.helpText}>
|
||||
{translate('AddNewMessage')}
|
||||
</div>
|
||||
<div>
|
||||
{translate('AddNewTmdbIdMessage')}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!term && !hasExistingMovies ?
|
||||
<div className={styles.message}>
|
||||
<div className={styles.noMoviesText}>
|
||||
{translate('HaveNotAddedMovies')}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
to="/add/import"
|
||||
kind={kinds.PRIMARY}
|
||||
>
|
||||
{translate('ImportExistingMovies')}
|
||||
</Button>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<div />
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewMovie.propTypes = {
|
||||
term: PropTypes.string,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasExistingMovies: PropTypes.bool.isRequired,
|
||||
onMovieLookupChange: PropTypes.func.isRequired,
|
||||
onClearMovieLookup: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewMovie;
|
||||
@@ -1,108 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
import AddNewMovie from './AddNewMovie';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addMovie,
|
||||
(state) => state.movies.items.length,
|
||||
(state) => state.router.location,
|
||||
(addMovie, existingMoviesCount, location) => {
|
||||
const { params } = parseUrl(location.search);
|
||||
|
||||
return {
|
||||
...addMovie,
|
||||
term: params.term,
|
||||
hasExistingMovies: existingMoviesCount > 0
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
lookupMovie,
|
||||
clearAddMovie,
|
||||
fetchRootFolders,
|
||||
fetchImportExclusions
|
||||
};
|
||||
|
||||
class AddNewMovieConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._movieLookupTimeout = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
this.props.fetchImportExclusions();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._movieLookupTimeout) {
|
||||
clearTimeout(this._movieLookupTimeout);
|
||||
}
|
||||
|
||||
this.props.clearAddMovie();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMovieLookupChange = (term) => {
|
||||
if (this._movieLookupTimeout) {
|
||||
clearTimeout(this._movieLookupTimeout);
|
||||
}
|
||||
|
||||
if (term.trim() === '') {
|
||||
this.props.clearAddMovie();
|
||||
} else {
|
||||
this._movieLookupTimeout = setTimeout(() => {
|
||||
this.props.lookupMovie({ term });
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
onClearMovieLookup = () => {
|
||||
this.props.clearAddMovie();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
term,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AddNewMovie
|
||||
term={term}
|
||||
{...otherProps}
|
||||
onMovieLookupChange={this.onMovieLookupChange}
|
||||
onClearMovieLookup={this.onClearMovieLookup}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewMovieConnector.propTypes = {
|
||||
term: PropTypes.string,
|
||||
lookupMovie: PropTypes.func.isRequired,
|
||||
clearAddMovie: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
fetchImportExclusions: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
|
||||
@@ -1,31 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import AddNewMovieModalContentConnector from './AddNewMovieModalContentConnector';
|
||||
|
||||
function AddNewMovieModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AddNewMovieModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AddNewMovieModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewMovieModal;
|
||||
@@ -1,68 +0,0 @@
|
||||
.container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 5px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.labelIcon {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.searchForMissingMovieLabelContainer {
|
||||
display: flex;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.searchForMissingMovieLabel {
|
||||
margin-right: 8px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.searchForMissingMovieContainer {
|
||||
composes: container from '~Components/Form/CheckInput.css';
|
||||
|
||||
flex: 0 1 0;
|
||||
}
|
||||
|
||||
.searchForMissingMovieInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modalFooter {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
}
|
||||
|
||||
.addButton {
|
||||
@add-mixin truncate;
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.modalFooter {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddNewMovieModalContent.css';
|
||||
|
||||
class AddNewMovieModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
searchForMovie: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSearchForMissingMovieChange = ({ value }) => {
|
||||
this.setState({ searchForMovie: value });
|
||||
}
|
||||
|
||||
onQualityProfileIdChange = ({ value }) => {
|
||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||
}
|
||||
|
||||
onAddMoviePress = () => {
|
||||
this.props.onAddMoviePress(this.state.searchForMovie);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
year,
|
||||
overview,
|
||||
images,
|
||||
isAdding,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
folder,
|
||||
tags,
|
||||
isSmallScreen,
|
||||
isWindows,
|
||||
onModalClose,
|
||||
onInputChange
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{title}
|
||||
|
||||
{
|
||||
!title.contains(year) && !!year &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
<div className={styles.poster}>
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RootFolder')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||
name="rootFolderPath"
|
||||
valueOptions={{
|
||||
movieFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
selectedValueOptions={{
|
||||
movieFolder: folder,
|
||||
isWindows
|
||||
}}
|
||||
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
||||
onChange={onInputChange}
|
||||
{...rootFolderPath}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Monitor
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
||||
name="monitor"
|
||||
onChange={onInputChange}
|
||||
{...monitor}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.AVAILABILITY_SELECT}
|
||||
name="minimumAvailability"
|
||||
onChange={onInputChange}
|
||||
{...minimumAvailability}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('QualityProfile')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.QUALITY_PROFILE_SELECT}
|
||||
name="qualityProfileId"
|
||||
onChange={this.onQualityProfileIdChange}
|
||||
{...qualityProfileId}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Tags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
onChange={onInputChange}
|
||||
{...tags}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<label className={styles.searchForMissingMovieLabelContainer}>
|
||||
<span className={styles.searchForMissingMovieLabel}>
|
||||
Start search for missing movie
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
containerClassName={styles.searchForMissingMovieContainer}
|
||||
className={styles.searchForMissingMovieInput}
|
||||
name="searchForMovie"
|
||||
value={this.state.searchForMovie}
|
||||
onChange={this.onSearchForMissingMovieChange}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.addButton}
|
||||
kind={kinds.SUCCESS}
|
||||
isSpinning={isAdding}
|
||||
onPress={this.onAddMoviePress}
|
||||
>
|
||||
Add {title}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewMovieModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
overview: PropTypes.string,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isWindows: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onAddMoviePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddNewMovieModalContent;
|
||||
@@ -1,103 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { addMovie, setAddMovieDefault } from 'Store/Actions/addMovieActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
import AddNewMovieModalContent from './AddNewMovieModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.addMovie,
|
||||
createDimensionsSelector(),
|
||||
createSystemStatusSelector(),
|
||||
(addMovieState, dimensions, systemStatus) => {
|
||||
const {
|
||||
isAdding,
|
||||
addError,
|
||||
defaults
|
||||
} = addMovieState;
|
||||
|
||||
const {
|
||||
settings,
|
||||
validationErrors,
|
||||
validationWarnings
|
||||
} = selectSettings(defaults, {}, addError);
|
||||
|
||||
return {
|
||||
isAdding,
|
||||
addError,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
validationErrors,
|
||||
validationWarnings,
|
||||
isWindows: systemStatus.isWindows,
|
||||
...settings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setAddMovieDefault,
|
||||
addMovie
|
||||
};
|
||||
|
||||
class AddNewMovieModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setAddMovieDefault({ [name]: value });
|
||||
}
|
||||
|
||||
onAddMoviePress = (searchForMovie) => {
|
||||
const {
|
||||
tmdbId,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
minimumAvailability,
|
||||
tags
|
||||
} = this.props;
|
||||
|
||||
this.props.addMovie({
|
||||
tmdbId,
|
||||
rootFolderPath: rootFolderPath.value,
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
minimumAvailability: minimumAvailability.value,
|
||||
tags: tags.value,
|
||||
searchForMovie
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AddNewMovieModalContent
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onAddMoviePress={this.onAddMoviePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewMovieModalContentConnector.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
rootFolderPath: PropTypes.object,
|
||||
monitor: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.object,
|
||||
minimumAvailability: PropTypes.object.isRequired,
|
||||
tags: PropTypes.object.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
setAddMovieDefault: PropTypes.func.isRequired,
|
||||
addMovie: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieModalContentConnector);
|
||||
@@ -1,94 +0,0 @@
|
||||
.searchResult {
|
||||
position: relative;
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.underlay {
|
||||
@add-mixin cover;
|
||||
|
||||
background-color: $white;
|
||||
transition: background 500ms;
|
||||
|
||||
&:hover {
|
||||
background-color: #eaf2ff;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
@add-mixin linkOverlay;
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.poster {
|
||||
flex: 0 0 170px;
|
||||
margin-right: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 0 1 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 10px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex: 1 0 auto;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.exclusionIcon {
|
||||
margin-left: 10px;
|
||||
color: $dangerColor;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.overview {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-left: 8px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.titleRow {
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HeartRating from 'Components/HeartRating';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AddNewMovieModal from './AddNewMovieModal';
|
||||
import styles from './AddNewMovieSearchResult.css';
|
||||
|
||||
class AddNewMovieSearchResult extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isNewAddMovieModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.isExistingMovie && this.props.isExistingMovie) {
|
||||
this.onAddMovieModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: true });
|
||||
}
|
||||
|
||||
onAddMovieModalClose = () => {
|
||||
this.setState({ isNewAddMovieModalOpen: false });
|
||||
}
|
||||
|
||||
onExternalLinkPress = (event) => {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
imdbId,
|
||||
youTubeTrailerId,
|
||||
title,
|
||||
titleSlug,
|
||||
year,
|
||||
studio,
|
||||
status,
|
||||
overview,
|
||||
ratings,
|
||||
folder,
|
||||
images,
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isNewAddMovieModalOpen
|
||||
} = this.state;
|
||||
|
||||
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
||||
|
||||
return (
|
||||
<div className={styles.searchResult}>
|
||||
<Link
|
||||
className={styles.underlay}
|
||||
{...linkProps}
|
||||
/>
|
||||
|
||||
<div className={styles.overlay}>
|
||||
{
|
||||
isSmallScreen ?
|
||||
null :
|
||||
<MoviePoster
|
||||
className={styles.poster}
|
||||
images={images}
|
||||
size={250}
|
||||
overflow={true}
|
||||
/>
|
||||
}
|
||||
|
||||
<div className={styles.content}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
|
||||
{
|
||||
!title.contains(year) && !!year ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.icons}>
|
||||
|
||||
{
|
||||
isExistingMovie &&
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title={translate('AlreadyInYourLibrary')}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isExclusionMovie &&
|
||||
<Icon
|
||||
className={styles.exclusionIcon}
|
||||
name={icons.DANGER}
|
||||
size={36}
|
||||
title={translate('MovieIsOnImportExclusionList')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label size={sizes.LARGE}>
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
!!studio &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{studio}
|
||||
</Label>
|
||||
}
|
||||
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Label
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
Links
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
<MovieDetailsLinks
|
||||
tmdbId={tmdbId}
|
||||
youTubeTrailerId={youTubeTrailerId}
|
||||
imdbId={imdbId}
|
||||
/>
|
||||
}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<Label
|
||||
kind={kinds.DANGER}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Ended
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.overview}>
|
||||
{overview}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AddNewMovieModal
|
||||
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
|
||||
tmdbId={tmdbId}
|
||||
title={title}
|
||||
year={year}
|
||||
overview={overview}
|
||||
folder={folder}
|
||||
images={images}
|
||||
onModalClose={this.onAddMovieModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddNewMovieSearchResult.propTypes = {
|
||||
tmdbId: PropTypes.number.isRequired,
|
||||
imdbId: PropTypes.string,
|
||||
youTubeTrailerId: PropTypes.string,
|
||||
title: PropTypes.string.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
studio: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingMovie: PropTypes.bool.isRequired,
|
||||
isExclusionMovie: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AddNewMovieSearchResult;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createExclusionMovieSelector from 'Store/Selectors/createExclusionMovieSelector';
|
||||
import createExistingMovieSelector from 'Store/Selectors/createExistingMovieSelector';
|
||||
import AddNewMovieSearchResult from './AddNewMovieSearchResult';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingMovieSelector(),
|
||||
createExclusionMovieSelector(),
|
||||
createDimensionsSelector(),
|
||||
(isExistingMovie, isExclusionMovie, dimensions) => {
|
||||
return {
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(AddNewMovieSearchResult);
|
||||
@@ -1,177 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import ImportMovieFooterConnector from './ImportMovieFooterConnector';
|
||||
import ImportMovieTableConnector from './ImportMovieTableConnector';
|
||||
|
||||
class ImportMovie extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
contentBody: null
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState, { parseIds: false });
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
// Only select non-dupes
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveSelectedStateItem = (id) => {
|
||||
this.setState((state) => {
|
||||
const selectedState = Object.assign({}, state.selectedState);
|
||||
delete selectedState[id];
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedState
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onInputChange(this.getSelectedIds(), name, value);
|
||||
}
|
||||
|
||||
onImportPress = () => {
|
||||
this.props.onImportPress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
rootFolderId,
|
||||
path,
|
||||
rootFoldersFetching,
|
||||
rootFoldersError,
|
||||
rootFoldersPopulated,
|
||||
unmappedFolders
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
scroller
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title={translate('ImportMovies')}>
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{
|
||||
rootFoldersFetching ? <LoadingIndicator /> : null
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersFetching && !!rootFoldersError ?
|
||||
<div>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersPopulated &&
|
||||
!unmappedFolders.length ?
|
||||
<div>
|
||||
All movies in {path} have been imported
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
rootFoldersPopulated &&
|
||||
!!unmappedFolders.length &&
|
||||
scroller ?
|
||||
<ImportMovieTableConnector
|
||||
rootFolderId={rootFolderId}
|
||||
unmappedFolders={unmappedFolders}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
selectedState={selectedState}
|
||||
scroller={scroller}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
!rootFoldersError &&
|
||||
!rootFoldersFetching &&
|
||||
!!unmappedFolders.length ?
|
||||
<ImportMovieFooterConnector
|
||||
selectedIds={this.getSelectedIds()}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportMovie.propTypes = {
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
path: PropTypes.string,
|
||||
rootFoldersFetching: PropTypes.bool.isRequired,
|
||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||
rootFoldersError: PropTypes.object,
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object),
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ImportMovie.defaultProps = {
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
export default ImportMovie;
|
||||
@@ -1,153 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape';
|
||||
import { setAddMovieDefault } from 'Store/Actions/addMovieActions';
|
||||
import { clearImportMovie, importMovie, setImportMovieValue } from 'Store/Actions/importMovieActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import ImportMovie from './ImportMovie';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { match }) => match,
|
||||
(state) => state.rootFolders,
|
||||
(state) => state.addMovie,
|
||||
(state) => state.importMovie,
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(
|
||||
match,
|
||||
rootFolders,
|
||||
addMovie,
|
||||
importMovieState,
|
||||
qualityProfiles
|
||||
) => {
|
||||
const {
|
||||
isFetching: rootFoldersFetching,
|
||||
isPopulated: rootFoldersPopulated,
|
||||
error: rootFoldersError,
|
||||
items
|
||||
} = rootFolders;
|
||||
|
||||
const rootFolderId = parseInt(match.params.rootFolderId);
|
||||
|
||||
const result = {
|
||||
rootFolderId,
|
||||
rootFoldersFetching,
|
||||
rootFoldersPopulated,
|
||||
rootFoldersError,
|
||||
qualityProfiles: qualityProfiles.items,
|
||||
defaultQualityProfileId: addMovie.defaults.qualityProfileId
|
||||
};
|
||||
|
||||
if (items.length) {
|
||||
const rootFolder = _.find(items, { id: rootFolderId });
|
||||
|
||||
return {
|
||||
...result,
|
||||
...rootFolder,
|
||||
items: importMovieState.items
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetImportMovieValue: setImportMovieValue,
|
||||
dispatchImportMovie: importMovie,
|
||||
dispatchClearImportMovie: clearImportMovie,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchSetAddMovieDefault: setAddMovieDefault
|
||||
};
|
||||
|
||||
class ImportMovieConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
rootFolderId,
|
||||
qualityProfiles,
|
||||
defaultQualityProfileId,
|
||||
dispatchFetchRootFolders,
|
||||
dispatchSetAddMovieDefault
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchRootFolders({ id: rootFolderId, timeout: false });
|
||||
|
||||
let setDefaults = false;
|
||||
const setDefaultPayload = {};
|
||||
|
||||
if (
|
||||
!defaultQualityProfileId ||
|
||||
!qualityProfiles.some((p) => p.id === defaultQualityProfileId)
|
||||
) {
|
||||
setDefaults = true;
|
||||
setDefaultPayload.qualityProfileId = qualityProfiles[0].id;
|
||||
}
|
||||
|
||||
if (setDefaults) {
|
||||
dispatchSetAddMovieDefault(setDefaultPayload);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearImportMovie();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = (ids, name, value) => {
|
||||
this.props.dispatchSetAddMovieDefault({ [name]: value });
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.dispatchSetImportMovieValue({
|
||||
id,
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
onImportPress = (ids) => {
|
||||
this.props.dispatchImportMovie({ ids });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportMovie
|
||||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onImportPress={this.onImportPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const routeMatchShape = createRouteMatchShape({
|
||||
rootFolderId: PropTypes.string.isRequired
|
||||
});
|
||||
|
||||
ImportMovieConnector.propTypes = {
|
||||
match: routeMatchShape.isRequired,
|
||||
rootFolderId: PropTypes.number.isRequired,
|
||||
rootFoldersFetching: PropTypes.bool.isRequired,
|
||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
defaultQualityProfileId: PropTypes.number.isRequired,
|
||||
dispatchSetImportMovieValue: PropTypes.func.isRequired,
|
||||
dispatchImportMovie: PropTypes.func.isRequired,
|
||||
dispatchClearImportMovie: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchSetAddMovieDefault: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportMovieConnector);
|
||||