mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-04-17 21:26:13 -04:00
Compare commits
419 Commits
phantom-jo
...
phantom-di
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f8e38d5a | ||
|
|
031371652b | ||
|
|
02104aff34 | ||
|
|
5bd1c47ca7 | ||
|
|
f846e0c031 | ||
|
|
72b0f640f4 | ||
|
|
430af0401c | ||
|
|
0ff08dbe8d | ||
|
|
57cca9fcdc | ||
|
|
b3daa280c5 | ||
|
|
449c1caf55 | ||
|
|
0c05236bee | ||
|
|
9f54ff8169 | ||
|
|
f2e1b4e435 | ||
|
|
1e80361c3a | ||
|
|
3564be19a8 | ||
|
|
14c9b6aaf4 | ||
|
|
069fc5cd33 | ||
|
|
3586d7042b | ||
|
|
a32b6276bd | ||
|
|
b0e31629b5 | ||
|
|
e2644c3847 | ||
|
|
7ea45bb714 | ||
|
|
f524fcd3e4 | ||
|
|
6c418302f8 | ||
|
|
b28d329654 | ||
|
|
ebe2ad1520 | ||
|
|
41dfb677e7 | ||
|
|
c646bef369 | ||
|
|
910de6d94a | ||
|
|
5951992bd5 | ||
|
|
cb9d78064a | ||
|
|
fd608fd411 | ||
|
|
d3bd90e4b9 | ||
|
|
4988655568 | ||
|
|
098db08ede | ||
|
|
93e3e92bba | ||
|
|
bdfdd28d6a | ||
|
|
a75e10c4c9 | ||
|
|
5251db7224 | ||
|
|
4d1a4d4241 | ||
|
|
d3a22459ac | ||
|
|
4f7e00bdc4 | ||
|
|
1199ae4e4f | ||
|
|
36088ef49d | ||
|
|
7ffb2eb440 | ||
|
|
0716d0931a | ||
|
|
66ee28d0a9 | ||
|
|
1487f54749 | ||
|
|
013c46d266 | ||
|
|
c8d2fcb223 | ||
|
|
5288b61378 | ||
|
|
a2679f64ee | ||
|
|
5d9dfee3c0 | ||
|
|
98f9323b42 | ||
|
|
f282ae8aae | ||
|
|
0b1e99991e | ||
|
|
75be036a87 | ||
|
|
23dc7794f1 | ||
|
|
b8e2f3d716 | ||
|
|
686a14cdff | ||
|
|
b4405b0600 | ||
|
|
d4bcf28d08 | ||
|
|
4bacc35605 | ||
|
|
417340c2c6 | ||
|
|
79d8a9d44b | ||
|
|
68440bba4d | ||
|
|
0719c83da4 | ||
|
|
5a7dec34cc | ||
|
|
9d766cfed5 | ||
|
|
1498f4e361 | ||
|
|
05dd17aacb | ||
|
|
200aee52f7 | ||
|
|
d6dd13a6be | ||
|
|
be3b3df903 | ||
|
|
f2a56b29d9 | ||
|
|
27d98868b8 | ||
|
|
7f28ab895a | ||
|
|
97ec184754 | ||
|
|
42343d5283 | ||
|
|
479baf06a7 | ||
|
|
7f7d196e44 | ||
|
|
c862fd9ff6 | ||
|
|
770b89c2b3 | ||
|
|
576275b6da | ||
|
|
776191b3bd | ||
|
|
d369d85699 | ||
|
|
552fac0466 | ||
|
|
a348d98dd9 | ||
|
|
ccdfdd1049 | ||
|
|
f0ca636654 | ||
|
|
b5e734b9e5 | ||
|
|
e1639d35a2 | ||
|
|
9b99ad27cd | ||
|
|
bba57bb434 | ||
|
|
8c24cd9864 | ||
|
|
91de7ff11c | ||
|
|
9702d2e5ad | ||
|
|
638066db03 | ||
|
|
1b3839ac0d | ||
|
|
219494ea9d | ||
|
|
642f75761f | ||
|
|
ed28f94f02 | ||
|
|
618c611a59 | ||
|
|
00821b7ad6 | ||
|
|
84b9488cfb | ||
|
|
37ad801065 | ||
|
|
4219cdb364 | ||
|
|
e23a879669 | ||
|
|
4ddf4a22a3 | ||
|
|
72afb28c30 | ||
|
|
eb51a42f60 | ||
|
|
bc01384cc7 | ||
|
|
00c922875f | ||
|
|
8c93d73b42 | ||
|
|
3b6d60e904 | ||
|
|
a965b8e7b2 | ||
|
|
25abf52b3f | ||
|
|
19764014be | ||
|
|
c91a5c80d3 | ||
|
|
e7b88c313d | ||
|
|
9ac0864b61 | ||
|
|
fcdd0f21c7 | ||
|
|
5497b68a98 | ||
|
|
50886ac928 | ||
|
|
e2ff089232 | ||
|
|
ae7f8926f8 | ||
|
|
0bbc4e8c1b | ||
|
|
295fdad750 | ||
|
|
63e01aff8c | ||
|
|
e05ceb226c | ||
|
|
1c699841c1 | ||
|
|
385c7fb0ce | ||
|
|
15d84046db | ||
|
|
3ad396a9c2 | ||
|
|
77f886ceef | ||
|
|
8adb788205 | ||
|
|
d731317c81 | ||
|
|
a824ce691b | ||
|
|
506023b0f3 | ||
|
|
52e5d4d0f1 | ||
|
|
00edffc0f4 | ||
|
|
92f1f3e73a | ||
|
|
1d339ad4f1 | ||
|
|
99728a604d | ||
|
|
c07a67ae3c | ||
|
|
be11789a86 | ||
|
|
b8ce274fa5 | ||
|
|
d7967e3e1b | ||
|
|
746da69070 | ||
|
|
b05b7ec4ad | ||
|
|
9abdaca079 | ||
|
|
5a79b8502e | ||
|
|
466d4fba9e | ||
|
|
108f6fe393 | ||
|
|
792896c46b | ||
|
|
43d04cd54e | ||
|
|
283f905d79 | ||
|
|
dd8d1b673e | ||
|
|
9ef64660ce | ||
|
|
88b1c8fc3e | ||
|
|
bcc8b655f7 | ||
|
|
438d9eb717 | ||
|
|
2c0a0175ef | ||
|
|
e51f1b5e16 | ||
|
|
544108df37 | ||
|
|
a23639e62e | ||
|
|
cde5a6d1a4 | ||
|
|
b601c8bcfe | ||
|
|
023c8260f2 | ||
|
|
51e2e084af | ||
|
|
fc5dd8137f | ||
|
|
268fc46ef7 | ||
|
|
010c65af9c | ||
|
|
db42256dc3 | ||
|
|
e9b537b6e6 | ||
|
|
c615ef476a | ||
|
|
b93e8da235 | ||
|
|
74a0a57468 | ||
|
|
b19d665817 | ||
|
|
10dc884fa8 | ||
|
|
d8446c2d5a | ||
|
|
d3cd46bb51 | ||
|
|
bc0da03caf | ||
|
|
c0a356261b | ||
|
|
fa4060b7fe | ||
|
|
29117fc222 | ||
|
|
24ba5e5bda | ||
|
|
2d94857369 | ||
|
|
c6ea7d7e63 | ||
|
|
3916495329 | ||
|
|
4e965e59a9 | ||
|
|
0acb3aa32b | ||
|
|
9189d8bf4d | ||
|
|
ec0c96bde4 | ||
|
|
562c8c4afe | ||
|
|
fd6d4493c4 | ||
|
|
1a2419e096 | ||
|
|
b86cfd49ef | ||
|
|
d421ff9736 | ||
|
|
92c61701f2 | ||
|
|
d45d9e356c | ||
|
|
098f9a2675 | ||
|
|
0347dab82e | ||
|
|
9aa89a0df9 | ||
|
|
556bd11725 | ||
|
|
07f5c21a07 | ||
|
|
93b20960b8 | ||
|
|
3cbdd6bfd3 | ||
|
|
c3c38880e6 | ||
|
|
415bbf5b3b | ||
|
|
186cb02748 | ||
|
|
4aaccb909f | ||
|
|
2daf7dd01a | ||
|
|
ab9ed73e55 | ||
|
|
a4a33fe167 | ||
|
|
e6fbd10031 | ||
|
|
9868d96fec | ||
|
|
0d1c2ac40c | ||
|
|
a6d0dddaf7 | ||
|
|
06d57e8f32 | ||
|
|
70a40edc5d | ||
|
|
95d64208d0 | ||
|
|
e28b2e8328 | ||
|
|
4123745a6b | ||
|
|
70bb4d71e6 | ||
|
|
dd314e1741 | ||
|
|
3cbb489ac6 | ||
|
|
101df4cbf1 | ||
|
|
42263a0ec0 | ||
|
|
d402f7514e | ||
|
|
84e6674e23 | ||
|
|
afcfaace19 | ||
|
|
5a3bd8cfe5 | ||
|
|
f0c90a4744 | ||
|
|
3baed292e1 | ||
|
|
d41a2cad73 | ||
|
|
ffccc3be38 | ||
|
|
ef1e8d7ef3 | ||
|
|
7af891216d | ||
|
|
aa80500b35 | ||
|
|
b72fbe06f7 | ||
|
|
41a63a5418 | ||
|
|
e8ce7898c1 | ||
|
|
687a45c564 | ||
|
|
3ac3dd3ca5 | ||
|
|
f2efebf7d9 | ||
|
|
7b68ce49d5 | ||
|
|
8a2a41fab0 | ||
|
|
ceaaec5378 | ||
|
|
e49a3e7206 | ||
|
|
dc7986dbad | ||
|
|
2dfba130f5 | ||
|
|
155c7c409b | ||
|
|
aacb8970f8 | ||
|
|
be66a0520d | ||
|
|
3fa3c45794 | ||
|
|
0f6da1873e | ||
|
|
1564208e83 | ||
|
|
e724e8db60 | ||
|
|
5a092a83cd | ||
|
|
ffefe5e8aa | ||
|
|
631fdd8a26 | ||
|
|
53d7ef4014 | ||
|
|
5c3ac79043 | ||
|
|
90fb1646e0 | ||
|
|
54604e45e0 | ||
|
|
9ed0f9eee8 | ||
|
|
b764c44318 | ||
|
|
adbd519061 | ||
|
|
b0415299ca | ||
|
|
e96d05149c | ||
|
|
354ddcfee5 | ||
|
|
6d232778e2 | ||
|
|
95ee7daf21 | ||
|
|
2238ac5d17 | ||
|
|
c209c1c034 | ||
|
|
b1eec16333 | ||
|
|
e126c45fb3 | ||
|
|
c89ff93be4 | ||
|
|
c82c27a5c5 | ||
|
|
b3e84f407a | ||
|
|
72902c8984 | ||
|
|
2c47c5eb99 | ||
|
|
398129f3e1 | ||
|
|
d74ab12d9e | ||
|
|
679c0599dd | ||
|
|
4d04ad5632 | ||
|
|
3fdc50b354 | ||
|
|
7eeff32185 | ||
|
|
d40f2cb852 | ||
|
|
f9dc2fb6d5 | ||
|
|
de31dfb11e | ||
|
|
ef6a648189 | ||
|
|
09953e2af8 | ||
|
|
be240119e8 | ||
|
|
2b7893c834 | ||
|
|
896e824ca1 | ||
|
|
7a94725808 | ||
|
|
a66fb76e9a | ||
|
|
b453d48fee | ||
|
|
a7f2c07998 | ||
|
|
3cff878f74 | ||
|
|
665d536481 | ||
|
|
ec6d407fbb | ||
|
|
72bc7ed6d4 | ||
|
|
ac407ca2c0 | ||
|
|
6af5f2b528 | ||
|
|
8fd4a98fbe | ||
|
|
07d553fae3 | ||
|
|
34e0eea173 | ||
|
|
73e6db9a12 | ||
|
|
0df464ac03 | ||
|
|
78ee6afbae | ||
|
|
31be74e6d3 | ||
|
|
767a09894a | ||
|
|
0d410d107d | ||
|
|
7829b18b3c | ||
|
|
b2267a55ce | ||
|
|
eca016fe61 | ||
|
|
81723f7fa9 | ||
|
|
44c91fb90c | ||
|
|
7cb5bd9c95 | ||
|
|
8196f6b9db | ||
|
|
d72b16531b | ||
|
|
f333196efe | ||
|
|
6ea047dcb4 | ||
|
|
ea65867b23 | ||
|
|
c41200e762 | ||
|
|
4c70afbb53 | ||
|
|
0c1ce66053 | ||
|
|
fa8b8cebf9 | ||
|
|
72fa89ba76 | ||
|
|
dc7b4cebf2 | ||
|
|
3c1dd94915 | ||
|
|
3decbbac3a | ||
|
|
27f43569f5 | ||
|
|
bd9bded73b | ||
|
|
0ce81e1ab6 | ||
|
|
2926201694 | ||
|
|
059be2c853 | ||
|
|
dd09f31abb | ||
|
|
1da20da3ff | ||
|
|
341773830b | ||
|
|
c65452bb01 | ||
|
|
34d81356a3 | ||
|
|
c9b84a5202 | ||
|
|
5394cc2dc9 | ||
|
|
63141f339f | ||
|
|
079a0b56c3 | ||
|
|
66721affe7 | ||
|
|
d8ab23e9ba | ||
|
|
11b0a5c9cc | ||
|
|
d273a72cb3 | ||
|
|
08641a6694 | ||
|
|
9ac5c0d886 | ||
|
|
8a57d33223 | ||
|
|
1945b53e43 | ||
|
|
161e1820a8 | ||
|
|
22b5ac8622 | ||
|
|
f75a70e42b | ||
|
|
c9076606be | ||
|
|
853d22b947 | ||
|
|
82d6719d91 | ||
|
|
3f02c150e3 | ||
|
|
d7d46a93a7 | ||
|
|
dfd51635a6 | ||
|
|
c04366d505 | ||
|
|
fd89e88d40 | ||
|
|
894de923b9 | ||
|
|
c47e7cd91d | ||
|
|
e359347a3b | ||
|
|
d320017e3c | ||
|
|
e6c34f4311 | ||
|
|
9b0c945086 | ||
|
|
bc85f5de1d | ||
|
|
4df219161c | ||
|
|
06f157e634 | ||
|
|
54addbdd28 | ||
|
|
0e721917e7 | ||
|
|
4dc7089f89 | ||
|
|
30b5a35db2 | ||
|
|
dc8f81b536 | ||
|
|
a018770a18 | ||
|
|
5e4f7c5d8e | ||
|
|
1d9d665ed0 | ||
|
|
68477c09a7 | ||
|
|
574b9086d4 | ||
|
|
d74c323c66 | ||
|
|
8e85a1b84e | ||
|
|
6ce1cb4325 | ||
|
|
fbbd85d8b2 | ||
|
|
e611dc43c5 | ||
|
|
c86309cfc0 | ||
|
|
ec74e9bce0 | ||
|
|
f371e8a523 | ||
|
|
57a059eecb | ||
|
|
2cb149c647 | ||
|
|
ee5371b582 | ||
|
|
70e4dbe3bd | ||
|
|
18ead9a64f | ||
|
|
d2764cee2a | ||
|
|
082c098420 | ||
|
|
46a42e2901 | ||
|
|
c21cacd309 | ||
|
|
81ac359f71 | ||
|
|
f5b91c90bc | ||
|
|
0a92a3012e | ||
|
|
ff8fc237e2 | ||
|
|
b99d943b4d | ||
|
|
3199fe08e8 | ||
|
|
7503ce62af | ||
|
|
df8ca250aa | ||
|
|
c71b4bde86 | ||
|
|
3f67802e3d | ||
|
|
093ed23140 | ||
|
|
0f8dee7011 | ||
|
|
0cb557b716 | ||
|
|
8137a776b6 |
24
.gitattributes
vendored
24
.gitattributes
vendored
@@ -1,22 +1,12 @@
|
|||||||
# Auto detect text files and perform LF normalization
|
# Auto detect text files and perform LF normalization
|
||||||
*text eol=lf
|
* text=auto
|
||||||
|
|
||||||
|
# Explicitly set bash scripts to have unix endings
|
||||||
|
# when checked out on windows
|
||||||
|
*.sh text eol=lf
|
||||||
|
distribution/debian/* text eol=lf
|
||||||
|
macOS/Sonarr text eol=lf
|
||||||
|
|
||||||
# Custom for Visual Studio
|
# Custom for Visual Studio
|
||||||
*.cs diff=csharp
|
*.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
|
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -45,6 +45,10 @@ _dotCover*
|
|||||||
# DevExpress CodeRush
|
# DevExpress CodeRush
|
||||||
src/.cr/
|
src/.cr/
|
||||||
|
|
||||||
|
# Emacs
|
||||||
|
*~
|
||||||
|
\#*\#
|
||||||
|
|
||||||
# NCrunch
|
# NCrunch
|
||||||
*.ncrunch*
|
*.ncrunch*
|
||||||
.*crunch*.local.xml
|
.*crunch*.local.xml
|
||||||
@@ -115,7 +119,9 @@ node_modules/
|
|||||||
_output*
|
_output*
|
||||||
_rawPackage/
|
_rawPackage/
|
||||||
_dotTrace*
|
_dotTrace*
|
||||||
_tests/
|
_tests*
|
||||||
|
_publish*
|
||||||
|
_temp*
|
||||||
*.Result.xml
|
*.Result.xml
|
||||||
setup/Output/
|
setup/Output/
|
||||||
*.~is
|
*.~is
|
||||||
@@ -133,6 +139,5 @@ output/*
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
_start
|
_start
|
||||||
_temp_*/**/*
|
|
||||||
|
|
||||||
src/.idea/
|
src/.idea/
|
||||||
|
|||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,4 +0,0 @@
|
|||||||
[submodule "src/ExternalModules/CurlSharp"]
|
|
||||||
path = src/ExternalModules/CurlSharp
|
|
||||||
url = https://github.com/Sonarr/CurlSharp.git
|
|
||||||
branch = master
|
|
||||||
85
README.md
85
README.md
@@ -1,58 +1,73 @@
|
|||||||
# Sonarr
|
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
||||||
|
|
||||||
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||||
|
|
||||||
## Major Features Include:
|
## Getting Started
|
||||||
|
|
||||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
|
||||||
* Automatically detects new episodes
|
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
|
||||||
* Can scan your existing library and download any missing episodes
|
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
|
||||||
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
|
||||||
* Automatic failed download handling will try another release if one fails
|
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
|
||||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
|
||||||
* Fully configurable episode renaming
|
## Support
|
||||||
* Full integration with SABnzbd and NZBGet
|
|
||||||
* Full integration with Kodi, Plex (notification, library update, metadata)
|
- [Donate](https://sonarr.tv/donate)
|
||||||
* Full support for specials and multi-episode releases
|
- [Discord](https://discord.gg/M6BvZn5)
|
||||||
* And a beautiful UI
|
- [Reddit](https://www.reddit.com/r/sonarr)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Current Features
|
||||||
|
|
||||||
|
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||||
|
- Automatically detects new episodes
|
||||||
|
- Can scan your existing library and download any missing episodes
|
||||||
|
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
||||||
|
- Automatic failed download handling will try another release if one fails
|
||||||
|
- Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||||
|
- Fully configurable episode renaming
|
||||||
|
- Full integration with SABnzbd and NZBGet
|
||||||
|
- Full integration with Kodi, Plex (notification, library update, metadata)
|
||||||
|
- Full support for specials and multi-episode releases
|
||||||
|
- And a beautiful UI
|
||||||
|
|
||||||
## Configuring Development Environment:
|
## Configuring Development Environment:
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
* [Visual Studio 2017] (https://www.visualstudio.com/vs/)
|
- [Visual Studio 2017](https://www.visualstudio.com/vs)
|
||||||
* [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
* [NodeJS](https://nodejs.org/en/download/)
|
- [NodeJS](https://nodejs.org/en/download)
|
||||||
* [Yarn](https://yarnpkg.com/)
|
- [Yarn](https://yarnpkg.com)
|
||||||
|
|
||||||
### Setup
|
### Setup
|
||||||
|
|
||||||
* Make sure all the required software mentioned above are installed
|
- Make sure all the required software mentioned above are installed
|
||||||
* Clone the repository into your development machine. [*info*](https://help.github.com/en/articles/working-with-forks)
|
- Clone the repository recursively to get Sonarr and it's submodules
|
||||||
* Grab the submodules `git submodule init && git submodule update`
|
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
|
||||||
* Install the required Node Packages `yarn`
|
- Install the required Node Packages using `yarn`
|
||||||
|
|
||||||
### Backend Development
|
### Backend Development
|
||||||
|
|
||||||
* Run `yarn build` to build the UI
|
- Run `yarn build` to build the UI
|
||||||
* Open `Sonarr.sln` in Visual Studio
|
- Open `Sonarr.sln` in Visual Studio
|
||||||
* Make sure `NzbDrone.Console` is set as the startup project
|
- Make sure `Sonarr.Console` is set as the startup project
|
||||||
* Build `NzbDrone.Windows` and `NzbDrone.Mono` projects
|
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
|
||||||
* Build Solution
|
- Build Solution
|
||||||
|
|
||||||
### UI Development
|
### UI Development
|
||||||
|
|
||||||
* Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
||||||
* Run Sonarr.Console.exe (or debug in Visual Studio)
|
- Run Sonarr.Console.exe (or debug in Visual Studio)
|
||||||
|
|
||||||
### License
|
### Licenses
|
||||||
|
|
||||||
|
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
- Copyright 2010-2020
|
||||||
* Copyright 2010-2019
|
|
||||||
|
|
||||||
### Sponsors
|
### Sponsors
|
||||||
|
|
||||||
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||||
* [ReSharper](http://www.jetbrains.com/resharper/)
|
- [ReSharper](http://www.jetbrains.com/resharper/)
|
||||||
* [TeamCity](http://www.jetbrains.com/teamcity/)
|
- [TeamCity](http://www.jetbrains.com/teamcity/)
|
||||||
|
|||||||
185
build.sh
185
build.sh
@@ -1,15 +1,16 @@
|
|||||||
#! /bin/bash
|
#! /bin/bash
|
||||||
msBuildVersion='15.0'
|
msBuildVersion='15.0'
|
||||||
outputFolder='./_output'
|
outputFolder='./_output'
|
||||||
|
outputFolderWindows='./_output_windows'
|
||||||
outputFolderLinux='./_output_linux'
|
outputFolderLinux='./_output_linux'
|
||||||
outputFolderMacOS='./_output_macos'
|
outputFolderMacOS='./_output_macos'
|
||||||
outputFolderMacOSApp='./_output_macos_app'
|
outputFolderMacOSApp='./_output_macos_app'
|
||||||
testPackageFolder='./_tests/'
|
testPackageFolder='./_tests'
|
||||||
testSearchPattern='*.Test/bin/x86/Release'
|
testPackageFolderWindows='./_tests_windows'
|
||||||
|
testPackageFolderLinux='./_tests_linux'
|
||||||
sourceFolder='./src'
|
sourceFolder='./src'
|
||||||
slnFile=$sourceFolder/Sonarr.sln
|
slnFile=$sourceFolder/Sonarr.sln
|
||||||
updateFolder=$outputFolder/Sonarr.Update
|
updateSubFolder=Sonarr.Update
|
||||||
updateFolderMono=$outputFolderLinux/Sonarr.Update
|
|
||||||
|
|
||||||
nuget='tools/nuget/nuget.exe';
|
nuget='tools/nuget/nuget.exe';
|
||||||
vswhere='tools/vswhere/vswhere.exe';
|
vswhere='tools/vswhere/vswhere.exe';
|
||||||
@@ -47,7 +48,8 @@ UpdateVersionNumber()
|
|||||||
verBuild=`echo "${BUILD_NUMBER}" | cut -d. -f4`
|
verBuild=`echo "${BUILD_NUMBER}" | cut -d. -f4`
|
||||||
BUILD_NUMBER=$verMajorMinorRevision.$verBuild
|
BUILD_NUMBER=$verMajorMinorRevision.$verBuild
|
||||||
echo "##teamcity[buildNumber '$BUILD_NUMBER']"
|
echo "##teamcity[buildNumber '$BUILD_NUMBER']"
|
||||||
sed -i "s/^[[]assembly: Assembly\(File\|Informational\)\?Version[(]\"[0-9.*]\+\"[)]/[assembly: Assembly\1Version(\"$BUILD_NUMBER\")/g" ./src/NzbDrone*/Properties/AssemblyInfo.cs ./src/Sonarr*/Properties/AssemblyInfo.cs ./src/ServiceHelpers/*/Properties/AssemblyInfo.cs ./src/Common/CommonVersionInfo.cs
|
sed -i "s/<AssemblyVersion>[0-9.*]\+<\/AssemblyVersion>/<AssemblyVersion>$BUILD_NUMBER<\/AssemblyVersion>/g" ./src/Directory.Build.props
|
||||||
|
sed -i "s/<AssemblyConfiguration>[\$()A-Za-z-]\+<\/AssemblyConfiguration>/<AssemblyConfiguration>${BRANCH:-dev}<\/AssemblyConfiguration>/g" ./src/Directory.Build.props
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,13 +88,14 @@ CleanFolder()
|
|||||||
|
|
||||||
BuildWithMSBuild()
|
BuildWithMSBuild()
|
||||||
{
|
{
|
||||||
installationPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -property installationPath`
|
msBuildPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -find MSBuild\\\\\*\*\\\\Bin\\\\MSBuild.exe`
|
||||||
installationPath=${installationPath/C:\\/\/c\/}
|
msBuildPath=${msBuildPath/C:\\/\/c\/}
|
||||||
installationPath=${installationPath//\\/\/}
|
msBuildPath=${msBuildPath//\\/\/}
|
||||||
msBuild="$installationPath/MSBuild/$msBuildVersion/Bin"
|
msBuildDir=$(dirname "$msBuildPath")
|
||||||
echo $msBuild
|
|
||||||
|
|
||||||
export PATH=$msBuild:$PATH
|
echo $msBuildDir
|
||||||
|
|
||||||
|
export PATH=$msBuildDir:$PATH
|
||||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Clean //m
|
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Clean //m
|
||||||
$nuget restore $slnFile
|
$nuget restore $slnFile
|
||||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
|
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
|
||||||
@@ -101,15 +104,15 @@ BuildWithMSBuild()
|
|||||||
BuildWithXbuild()
|
BuildWithXbuild()
|
||||||
{
|
{
|
||||||
export MONO_IOMAP=case
|
export MONO_IOMAP=case
|
||||||
CheckExitCode xbuild /t:Clean $slnFile
|
CheckExitCode msbuild /t:Clean $slnFile
|
||||||
mono $nuget restore $slnFile
|
mono $nuget restore $slnFile
|
||||||
CheckExitCode xbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
CheckExitCode msbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||||
}
|
}
|
||||||
|
|
||||||
LintUI()
|
LintUI()
|
||||||
{
|
{
|
||||||
ProgressStart 'ESLint'
|
ProgressStart 'ESLint'
|
||||||
CheckExitCode yarn eslint
|
CheckExitCode yarn lint
|
||||||
ProgressEnd 'ESLint'
|
ProgressEnd 'ESLint'
|
||||||
|
|
||||||
ProgressStart 'Stylelint'
|
ProgressStart 'Stylelint'
|
||||||
@@ -122,6 +125,7 @@ Build()
|
|||||||
ProgressStart 'Build'
|
ProgressStart 'Build'
|
||||||
|
|
||||||
rm -rf $outputFolder
|
rm -rf $outputFolder
|
||||||
|
rm -rf $testPackageFolder
|
||||||
|
|
||||||
if [ $runtime = "dotnet" ] ; then
|
if [ $runtime = "dotnet" ] ; then
|
||||||
BuildWithMSBuild
|
BuildWithMSBuild
|
||||||
@@ -167,6 +171,48 @@ CreateMdbs()
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PatchMono()
|
||||||
|
{
|
||||||
|
local path=$1
|
||||||
|
|
||||||
|
# Below we deal with some mono incompatibilities with windows-only dotnet core/standard libs
|
||||||
|
# See: https://github.com/mono/mono/blob/master/tools/nuget-hash-extractor/download.sh
|
||||||
|
# That list defines assemblies that are prohibited from being loaded from the appdir, instead loading from mono GAC.
|
||||||
|
|
||||||
|
# We have debian dependencies to get these installed or facades from mono 5.10+
|
||||||
|
for assembly in System.IO.Compression System.Runtime.InteropServices.RuntimeInformation System.Net.Http System.Globalization.Extensions System.Text.Encoding.CodePages System.Threading.Overlapped
|
||||||
|
do
|
||||||
|
if [ -e $path/$assembly.dll ]; then
|
||||||
|
if [ -e $sourceFolder/Libraries/Mono/$assembly.dll ]; then
|
||||||
|
echo "Copy Mono-specific facade $assembly.dll (uses win32 interop)"
|
||||||
|
cp $sourceFolder/Libraries/Mono/$assembly.dll $path/$assembly.dll
|
||||||
|
else
|
||||||
|
echo "Remove $assembly.dll (uses win32 interop)"
|
||||||
|
rm $path/$assembly.dll
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Copy more stable version of Vectors for mono <5.12
|
||||||
|
if [ -e $path/System.Numerics.Vectors.dll ]; then
|
||||||
|
packageDir="$HOME/.nuget/packages/system.numerics.vectors/4.5.0"
|
||||||
|
|
||||||
|
if [ ! -d "$HOME/.nuget/packages/system.numerics.vectors/4.5.0" ]; then
|
||||||
|
# May reside in the NuGetFallback folder, which is harder to find
|
||||||
|
# Download somewhere to get the real cache populated
|
||||||
|
if [ $runtime = "dotnet" ] ; then
|
||||||
|
$nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors
|
||||||
|
else
|
||||||
|
mono $nuget install System.Numerics.Vectors -Version 4.5.0 -Output ./_temp/System.Numerics.Vectors
|
||||||
|
fi
|
||||||
|
rm -rf ./_temp/System.Numerics.Vectors
|
||||||
|
fi
|
||||||
|
# Copy the netstandard2.0 version rather than net46
|
||||||
|
cp "$packageDir/lib/netstandard2.0/System.Numerics.Vectors.dll" $path/
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
PackageMono()
|
PackageMono()
|
||||||
{
|
{
|
||||||
ProgressStart 'Creating Mono Package'
|
ProgressStart 'Creating Mono Package'
|
||||||
@@ -190,15 +236,14 @@ PackageMono()
|
|||||||
rm -f $outputFolderLinux/sqlite3.*
|
rm -f $outputFolderLinux/sqlite3.*
|
||||||
rm -f $outputFolderLinux/MediaInfo.*
|
rm -f $outputFolderLinux/MediaInfo.*
|
||||||
|
|
||||||
|
PatchMono $outputFolderLinux
|
||||||
|
|
||||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $outputFolderLinux
|
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $outputFolderLinux
|
||||||
|
|
||||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
# Remove Http binding redirect by renaming it
|
||||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $outputFolderLinux
|
# We don't need this anymore once our minimum mono version is 5.10
|
||||||
|
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $outputFolderLinux/Sonarr.Console.exe.config
|
||||||
echo "Adding unix System.Runtime.InteropServices.RuntimeInformation.dll (for SharpRaven)"
|
|
||||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux
|
|
||||||
cp $sourceFolder/packages/System.Runtime.InteropServices.RuntimeInformation.4.3.0/runtimes/unix/lib/netstandard1.1/System.Runtime.InteropServices.RuntimeInformation.dll $outputFolderLinux/Sonarr.Update
|
|
||||||
|
|
||||||
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
|
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
|
||||||
rm $outputFolderLinux/Sonarr.exe*
|
rm $outputFolderLinux/Sonarr.exe*
|
||||||
@@ -210,7 +255,7 @@ PackageMono()
|
|||||||
rm $outputFolderLinux/Sonarr.Windows.*
|
rm $outputFolderLinux/Sonarr.Windows.*
|
||||||
|
|
||||||
echo "Adding Sonarr.Mono to UpdatePackage"
|
echo "Adding Sonarr.Mono to UpdatePackage"
|
||||||
cp $outputFolderLinux/Sonarr.Mono.* $updateFolderMono
|
cp $outputFolderLinux/Sonarr.Mono.* $outputFolderLinux/$updateSubFolder/
|
||||||
|
|
||||||
ProgressEnd 'Creating Mono Package'
|
ProgressEnd 'Creating Mono Package'
|
||||||
}
|
}
|
||||||
@@ -266,54 +311,92 @@ PackageMacOSApp()
|
|||||||
ProgressEnd 'Creating macOS App Package'
|
ProgressEnd 'Creating macOS App Package'
|
||||||
}
|
}
|
||||||
|
|
||||||
PackageTests()
|
PackageTestsMono()
|
||||||
{
|
{
|
||||||
ProgressStart 'Creating Test Package'
|
ProgressStart 'Creating Mono Test Package'
|
||||||
|
|
||||||
rm -rf $testPackageFolder
|
rm -rf $testPackageFolderLinux
|
||||||
mkdir $testPackageFolder
|
|
||||||
|
|
||||||
find $sourceFolder -path $testSearchPattern -exec cp -r -u -T "{}" $testPackageFolder \;
|
echo "Copying Binaries"
|
||||||
|
cp -r $testPackageFolder $testPackageFolderLinux
|
||||||
|
|
||||||
if [ $runtime = "dotnet" ] ; then
|
if [ $runtime = "dotnet" ] ; then
|
||||||
$nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
|
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
|
||||||
else
|
else
|
||||||
mono $nuget install NUnit.ConsoleRunner -Version 3.2.0 -Output $testPackageFolder
|
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderLinux
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cp $outputFolder/*.dll $testPackageFolder
|
echo "Creating MDBs"
|
||||||
cp ./test.sh $testPackageFolder
|
CreateMdbs $testPackageFolderLinux
|
||||||
|
|
||||||
echo "Creating MDBs for tests"
|
echo "Removing PDBs"
|
||||||
CreateMdbs $testPackageFolder
|
find $testPackageFolderLinux -name "*.pdb" -exec rm "{}" \;
|
||||||
|
|
||||||
rm -f $testPackageFolder/*.log.config
|
PatchMono $testPackageFolderLinux
|
||||||
|
|
||||||
CleanFolder $testPackageFolder true
|
|
||||||
|
|
||||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolder
|
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolderLinux
|
||||||
|
|
||||||
echo "Adding CurlSharp.dll.config (for dllmap)"
|
# Remove Http binding redirect by renaming it
|
||||||
cp $sourceFolder/NzbDrone.Common/CurlSharp.dll.config $testPackageFolder
|
# We don't need this anymore once our minimum mono version is 5.10
|
||||||
|
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $testPackageFolderLinux/Sonarr.Common.Test.dll.config
|
||||||
|
|
||||||
echo "Copying CurlSharp libraries"
|
cp ./test.sh $testPackageFolderLinux/
|
||||||
cp $sourceFolder/ExternalModules/CurlSharp/libs/i386/* $testPackageFolder
|
dos2unix $testPackageFolderLinux/test.sh
|
||||||
|
|
||||||
ProgressEnd 'Creating Test Package'
|
echo "Removing Sonarr.Windows"
|
||||||
|
rm $testPackageFolderLinux/Sonarr.Windows.*
|
||||||
|
|
||||||
|
rm -f $testPackageFolderLinux/*.log.config
|
||||||
|
|
||||||
|
CleanFolder $testPackageFolderLinux true
|
||||||
|
|
||||||
|
ProgressEnd 'Creating Linux Test Package'
|
||||||
}
|
}
|
||||||
|
|
||||||
CleanupWindowsPackage()
|
PackageTestsWindows()
|
||||||
{
|
{
|
||||||
ProgressStart 'Cleaning Windows Package'
|
ProgressStart 'Creating Windows Test Package'
|
||||||
|
|
||||||
|
rm -rf $testPackageFolderWindows
|
||||||
|
|
||||||
|
echo "Copying Binaries"
|
||||||
|
cp -r $testPackageFolder $testPackageFolderWindows
|
||||||
|
|
||||||
|
if [ $runtime = "dotnet" ] ; then
|
||||||
|
$nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderWindows
|
||||||
|
else
|
||||||
|
mono $nuget install NUnit.ConsoleRunner -Version 3.10.0 -Output $testPackageFolderWindows
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp ./test.sh $testPackageFolderWindows
|
||||||
|
|
||||||
echo "Removing Sonarr.Mono"
|
echo "Removing Sonarr.Mono"
|
||||||
rm -f $outputFolder/Sonarr.Mono.*
|
rm -f $testPackageFolderWindows/Sonarr.Mono.*
|
||||||
|
|
||||||
|
rm -f $testPackageFolderWindows/*.log.config
|
||||||
|
|
||||||
|
CleanFolder $testPackageFolderWindows true
|
||||||
|
|
||||||
|
ProgressEnd 'Creating Windows Test Package'
|
||||||
|
}
|
||||||
|
|
||||||
|
PackageWindows()
|
||||||
|
{
|
||||||
|
ProgressStart 'Creating Windows Package'
|
||||||
|
|
||||||
|
rm -rf $outputFolderWindows
|
||||||
|
|
||||||
|
echo "Copying Binaries"
|
||||||
|
cp -r $outputFolder $outputFolderWindows
|
||||||
|
|
||||||
|
echo "Removing Sonarr.Mono"
|
||||||
|
rm -f $outputFolderWindows/Sonarr.Mono.*
|
||||||
|
|
||||||
echo "Adding Sonarr.Windows to UpdatePackage"
|
echo "Adding Sonarr.Windows to UpdatePackage"
|
||||||
cp $outputFolder/Sonarr.Windows.* $updateFolder
|
cp $outputFolderWindows/Sonarr.Windows.* $outputFolderWindows/$updateSubFolder/
|
||||||
|
|
||||||
ProgressEnd 'Cleaning Windows Package'
|
ProgressEnd 'Creating Windows Package'
|
||||||
}
|
}
|
||||||
|
|
||||||
PublishArtifacts()
|
PublishArtifacts()
|
||||||
@@ -321,10 +404,11 @@ PublishArtifacts()
|
|||||||
ProgressStart 'Publishing Artifacts'
|
ProgressStart 'Publishing Artifacts'
|
||||||
|
|
||||||
# Tests
|
# Tests
|
||||||
echo "##teamcity[publishArtifacts '_tests/** => tests.zip']"
|
echo "##teamcity[publishArtifacts '$testPackageFolderWindows/** => tests.windows.zip']"
|
||||||
|
echo "##teamcity[publishArtifacts '$testPackageFolderLinux/** => tests.linux.zip']"
|
||||||
|
|
||||||
# Releases
|
# Releases
|
||||||
echo "##teamcity[publishArtifacts '$outputFolder/** => Sonarr.$BRANCH.$BUILD_NUMBER.windows.zip!Sonarr']"
|
echo "##teamcity[publishArtifacts '$outputFolderWindows/** => Sonarr.$BRANCH.$BUILD_NUMBER.windows.zip!Sonarr']"
|
||||||
echo "##teamcity[publishArtifacts '$outputFolderLinux/** => Sonarr.$BRANCH.$BUILD_NUMBER.linux.tar.gz!Sonarr']"
|
echo "##teamcity[publishArtifacts '$outputFolderLinux/** => Sonarr.$BRANCH.$BUILD_NUMBER.linux.tar.gz!Sonarr']"
|
||||||
echo "##teamcity[publishArtifacts '$outputFolderMacOS/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.tar.gz!Sonarr']"
|
echo "##teamcity[publishArtifacts '$outputFolderMacOS/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.tar.gz!Sonarr']"
|
||||||
echo "##teamcity[publishArtifacts '$outputFolderMacOSApp/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.zip']"
|
echo "##teamcity[publishArtifacts '$outputFolderMacOSApp/** => Sonarr.$BRANCH.$BUILD_NUMBER.macos.zip']"
|
||||||
@@ -354,6 +438,7 @@ RunGulp
|
|||||||
PackageMono
|
PackageMono
|
||||||
PackageMacOS
|
PackageMacOS
|
||||||
PackageMacOSApp
|
PackageMacOSApp
|
||||||
PackageTests
|
PackageTestsMono
|
||||||
CleanupWindowsPackage
|
PackageTestsWindows
|
||||||
|
PackageWindows
|
||||||
PublishArtifacts
|
PublishArtifacts
|
||||||
|
|||||||
7
distribution/build-package.sh
Normal file
7
distribution/build-package.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Note, this script is only used for local dev tests, this is not the script used for building the official sonarr package
|
||||||
|
|
||||||
|
mkdir -p /${PWD}/../_output_debian
|
||||||
|
|
||||||
|
docker build -f docker-build/Dockerfile -t sonarr-packager ./docker-build
|
||||||
|
|
||||||
|
docker run --rm -v /${PWD}/../_output_linux:/data/sonarr_bin:ro -v /${PWD}:/data/build -v /${PWD}/../_output_debian:/data/output sonarr-packager
|
||||||
@@ -19,7 +19,11 @@ sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/
|
|||||||
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
||||||
|
|
||||||
echo Running debuild for $BuildVersion
|
echo Running debuild for $BuildVersion
|
||||||
debuild -b
|
if [ -z "${TEST_OUTPUT}" ]; then
|
||||||
|
debuild -b
|
||||||
|
else
|
||||||
|
debuild -us -uc -b
|
||||||
|
fi
|
||||||
|
|
||||||
# Restore debian directory to the original files
|
# Restore debian directory to the original files
|
||||||
rm -rf ./debian
|
rm -rf ./debian
|
||||||
@@ -32,16 +36,28 @@ sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debia
|
|||||||
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
||||||
|
|
||||||
echo Running debuild for $BootstrapVersion
|
echo Running debuild for $BootstrapVersion
|
||||||
debuild -b
|
if [ -z "${TEST_OUTPUT}" ]; then
|
||||||
|
debuild -b
|
||||||
|
else
|
||||||
|
debuild -us -uc -b
|
||||||
|
fi
|
||||||
|
|
||||||
echo Moving stuff around
|
echo Moving stuff around
|
||||||
mv ../sonarr_*.deb ./
|
mv ../sonarr_*.deb ./
|
||||||
mv ../sonarr_*.changes ./
|
mv ../sonarr_*.changes ./
|
||||||
rm ../sonarr_*.build
|
rm ../sonarr_*.build
|
||||||
|
|
||||||
echo Signing Package
|
if [ -z "${TEST_OUTPUT}" ]; then
|
||||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
echo Signing Package
|
||||||
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
dpkg-sig -k 884589CE --sign builder "sonarr_${BuildVersion}_all.deb"
|
||||||
|
dpkg-sig -k 884589CE --sign builder "sonarr_${BootstrapVersion}_all.deb"
|
||||||
|
|
||||||
echo running alien
|
echo running alien
|
||||||
alien -r -v ./*.deb
|
alien -r -v ./*.deb
|
||||||
|
else
|
||||||
|
echo "Exporting packages to ${TEST_OUTPUT}"
|
||||||
|
dpkg -e "sonarr_${BuildVersion}_all.deb" ${TEST_OUTPUT}/sonarr-build
|
||||||
|
dpkg -e "sonarr_${BootstrapVersion}_all.deb" ${TEST_OUTPUT}/sonarr-release
|
||||||
|
|
||||||
|
cp *.deb ${TEST_OUTPUT}/
|
||||||
|
fi
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ Vcs-Git: git@github.com:Sonarr/Sonarr.git
|
|||||||
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
Vcs-Browser: https://github.com/Sonarr/Sonarr
|
||||||
Build-Depends: debhelper (>= 9),
|
Build-Depends: debhelper (>= 9),
|
||||||
dh-systemd (>= 1.5),
|
dh-systemd (>= 1.5),
|
||||||
mono-devel (>= 4.6),
|
mono-devel (>= 5.18),
|
||||||
libmono-cil-dev (>= 4.6),
|
libmono-cil-dev (>= 5.18),
|
||||||
cli-common-dev (>= 0.5.7)
|
cli-common-dev (>= 0.9+xamarin5)
|
||||||
|
|
||||||
Package: sonarr
|
Package: sonarr
|
||||||
Architecture: all
|
Architecture: all
|
||||||
Provides: nzbdrone
|
Provides: nzbdrone
|
||||||
Conflicts: nzbdrone
|
Conflicts: nzbdrone
|
||||||
Replaces: nzbdrone
|
Replaces: nzbdrone
|
||||||
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52), mono-runtime (>= 5.4), ${cli:Depends}, ${misc:Depends}
|
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.18), ca-certificates-mono, libmono-system-net-http4.0-cil (>= 4.0.0~alpha1), ${cli:Depends}, ${misc:Depends}
|
||||||
Recommends: sqlite3 (>= 3.7), mediainfo (>= 0.7.52), ${cli:Recommends}
|
Recommends: libmediainfo0v5 (>= 18.03) | libmediainfo0 (>= 18.03)
|
||||||
|
Suggests: sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
|
||||||
Description: Internet PVR
|
Description: Internet PVR
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
sonarr_bin/* /usr/lib/sonarr/bin
|
sonarr_bin/* usr/lib/sonarr/bin
|
||||||
package_info /usr/lib/sonarr
|
package_info usr/lib/sonarr
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ chown -R $USER:$GROUP /usr/lib/sonarr
|
|||||||
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
|
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
|
||||||
|
|
||||||
#BEGIN BUILTIN UPDATER
|
#BEGIN BUILTIN UPDATER
|
||||||
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ]; then
|
if [ "$UPDATER" = "BuiltIn" ]; then
|
||||||
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
|
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
|
||||||
touch $CONFDIR/update_required
|
touch $CONFDIR/update_required
|
||||||
chown $USER:$GROUP $CONFDIR/update_required
|
chown $USER:$GROUP $CONFDIR/update_required
|
||||||
@@ -104,4 +104,4 @@ fi
|
|||||||
|
|
||||||
#DEBHELPER#
|
#DEBHELPER#
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -22,10 +22,25 @@ if [ $1 = "install" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
|
if [ "$psNzbDroneUnit" != "-" ] && [ -d /run/systemd/system ]; then
|
||||||
# The user used a systemd auto-startup for NzbDrone, we can deal with that.
|
if [ "$psNzbDroneUnit" = "sonarr.service" ]; then
|
||||||
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
# Conflicts with our new sonarr.service so we have to remove it
|
||||||
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and removing..."
|
||||||
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||||
|
if [ -f "/etc/systemd/system/$psNzbDroneUnit" ]; then
|
||||||
|
rm /etc/systemd/system/$psNzbDroneUnit
|
||||||
|
fi
|
||||||
|
if [ -f "/usr/lib/systemd/system/$psNzbDroneUnit" ]; then
|
||||||
|
rm /usr/lib/systemd/system/$psNzbDroneUnit
|
||||||
|
fi
|
||||||
|
deb-systemd-helper purge $psNzbDroneUnit >/dev/null
|
||||||
|
deb-systemd-helper unmask $psNzbDroneUnit >/dev/null
|
||||||
|
systemctl --system daemon-reload >/dev/null || true
|
||||||
|
else
|
||||||
|
# Just disable it, so the user can revisit the settings later
|
||||||
|
echo "NzbDrone systemd startup detected at $psNzbDroneUnit, stopping and disabling..."
|
||||||
|
deb-systemd-invoke stop $psNzbDroneUnit >/dev/null
|
||||||
|
deb-systemd-invoke mask $psNzbDroneUnit >/dev/null
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
# We don't support auto migration for other startup methods, so bail.
|
# We don't support auto migration for other startup methods, so bail.
|
||||||
# This leaves the sonarr package in an incomplete state.
|
# This leaves the sonarr package in an incomplete state.
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
#!/usr/bin/make -f
|
#!/usr/bin/make -f
|
||||||
# -*- makefile -*-
|
|
||||||
# Sample debian/rules that uses debhelper.
|
|
||||||
# This file was originally written by Joey Hess and Craig Small.
|
|
||||||
# As a special exception, when this file is copied by dh-make into a
|
|
||||||
# dh-make output file, you may use that output file without restriction.
|
|
||||||
# This special exception was added by Craig Small in version 0.37 of dh-make.
|
|
||||||
|
|
||||||
# Uncomment this to turn on verbose mode.
|
# Uncomment this to turn on verbose mode.
|
||||||
#export DH_VERBOSE=1
|
#export DH_VERBOSE=1
|
||||||
|
|
||||||
# Note: System.Native is a dependency of System.Runtime.InteropServices.RuntimeInformation used by SharpRaven,
|
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
|
||||||
# but SharpRaven doesn't use any functions that need System.Native
|
|
||||||
EXCLUDE_MODULEREFS = crypt32 httpapi System.Native
|
|
||||||
|
|
||||||
%:
|
%:
|
||||||
dh $@ --with=systemd --with=cli
|
dh $@ --with=systemd --with=cli
|
||||||
@@ -20,7 +12,7 @@ EXCLUDE_MODULEREFS = crypt32 httpapi System.Native
|
|||||||
override_dh_installinit:
|
override_dh_installinit:
|
||||||
true
|
true
|
||||||
|
|
||||||
# Sonarr like debug symbols for logging
|
# Sonarr likes debug symbols for logging
|
||||||
override_dh_clistrip:
|
override_dh_clistrip:
|
||||||
|
|
||||||
override_dh_makeclilibs:
|
override_dh_makeclilibs:
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
recommends libcurl3
|
ignores msbuild
|
||||||
ignores msbuild
|
ignores libmediainfo0v5
|
||||||
|
|||||||
23
distribution/docker-build/Dockerfile
Normal file
23
distribution/docker-build/Dockerfile
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
FROM ubuntu:xenial AS builder
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
ENV MONO_VERSION 5.18
|
||||||
|
|
||||||
|
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||||
|
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
devscripts build-essential tofrodos \
|
||||||
|
dh-make dh-systemd \
|
||||||
|
cli-common-dev \
|
||||||
|
mono-complete \
|
||||||
|
sqlite3 libcurl3 mediainfo
|
||||||
|
|
||||||
|
RUN apt-cache policy mono-complete
|
||||||
|
RUN apt-cache policy cli-common-dev
|
||||||
|
|
||||||
|
COPY debian-start.sh /debian-start.sh
|
||||||
|
RUN fromdos /debian-start.sh
|
||||||
|
|
||||||
|
WORKDIR /data
|
||||||
|
VOLUME [ "/data/sonarr_bin", "/data/build", "/data/output" ]
|
||||||
|
CMD /debian-start.sh
|
||||||
18
distribution/docker-build/debian-start.sh
Normal file
18
distribution/docker-build/debian-start.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
echo "Debian Build Dev bootstrap..."
|
||||||
|
|
||||||
|
export TEST_OUTPUT=/data/output
|
||||||
|
|
||||||
|
mkdir ${TEST_OUTPUT}
|
||||||
|
|
||||||
|
mkdir /data/temp
|
||||||
|
|
||||||
|
cp -rf /data/build/debian.sh /data/temp
|
||||||
|
cp -rf /data/build/debian /data/temp
|
||||||
|
cp -rf /data/sonarr_bin /data/temp/sonarr_bin
|
||||||
|
|
||||||
|
cd /data/temp
|
||||||
|
|
||||||
|
ls -al .
|
||||||
|
|
||||||
|
fromdos debian.sh
|
||||||
|
sh debian.sh
|
||||||
22
docker/tests/mono/complete/Dockerfile
Normal file
22
docker/tests/mono/complete/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM ubuntu:xenial
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
ARG MONO_VERSION=5.20
|
||||||
|
ARG MONO_URL=stable-xenial/snapshots/$MONO_VERSION
|
||||||
|
|
||||||
|
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||||
|
echo "deb http://download.mono-project.com/repo/debian $MONO_URL main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
tofrodos tzdata \
|
||||||
|
mono-complete \
|
||||||
|
sqlite3 mediainfo \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY startup.sh /startup.sh
|
||||||
|
RUN fromdos /startup.sh
|
||||||
|
|
||||||
|
WORKDIR /data/
|
||||||
|
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||||
|
|
||||||
|
CMD bash /startup.sh
|
||||||
|
|
||||||
29
docker/tests/mono/sonarr/Dockerfile
Normal file
29
docker/tests/mono/sonarr/Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM ubuntu:xenial
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
|
ARG MONO_VERSION=5.20
|
||||||
|
ARG MONO_URL=stable-xenial/snapshots/$MONO_VERSION
|
||||||
|
|
||||||
|
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
||||||
|
echo "deb http://download.mono-project.com/repo/debian $MONO_URL main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||||
|
apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 2009837CBFFD68F45BC180471F4F90DE2A9B4BF8 && \
|
||||||
|
echo "deb http://apt.sonarr.tv/ubuntu xenial main" > /etc/apt/sources.list.d/sonarr.list && \
|
||||||
|
apt-get update && apt-get install -y \
|
||||||
|
tofrodos tzdata \
|
||||||
|
sonarr \
|
||||||
|
sqlite3 mediainfo \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
libmono-system-runtime4.0-cil \
|
||||||
|
libmono-system-net-http4.0-cil \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY startup.sh /startup.sh
|
||||||
|
RUN fromdos /startup.sh
|
||||||
|
|
||||||
|
WORKDIR /data/
|
||||||
|
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||||
|
|
||||||
|
CMD bash /startup.sh
|
||||||
|
|
||||||
15
docker/tests/mono/startup.sh
Normal file
15
docker/tests/mono/startup.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
echo "Preparing Test..."
|
||||||
|
mkdir -p /data/test
|
||||||
|
cp -r /data/_tests_linux/* /data/test/
|
||||||
|
cp -r /data/_output_linux /data/test/bin
|
||||||
|
|
||||||
|
cd /data/test
|
||||||
|
|
||||||
|
runTest()
|
||||||
|
{
|
||||||
|
bash test.sh Linux $1
|
||||||
|
cp TestResult.xml /data/_tests_results/TestResult_$1.xml
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest Integration
|
||||||
|
runTest Unit
|
||||||
121
docker/tests/run-all.sh
Normal file
121
docker/tests/run-all.sh
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
|
||||||
|
opt_parallel=
|
||||||
|
opt_version=
|
||||||
|
opt_mode=both
|
||||||
|
while getopts 'pv:m:r?h' c
|
||||||
|
do
|
||||||
|
case $c in
|
||||||
|
p) opt_parallel=1 ;;
|
||||||
|
v) opt_version=$OPTARG ;;
|
||||||
|
m) opt_mode=$OPTARG ;;
|
||||||
|
r) opt_report=1 ;;
|
||||||
|
?|h) printf "Usage: %s [-p] [-v mono-ver] [-m sonarr|complete]\n" $0
|
||||||
|
printf " -p run parallel\n"
|
||||||
|
printf " -v run specified mono version\n"
|
||||||
|
printf " -m run only mono-'complete' or 'sonarr' package variants\n"
|
||||||
|
printf " -r only report\n"
|
||||||
|
exit 2
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
# NOTE:
|
||||||
|
# each container has a 1gb tmpfs mounted since it greatly speeds up the normally intensive db operations
|
||||||
|
# make sure that the docker host has enough memory to handle about ~300 MB per container, so 2-3 GB total
|
||||||
|
# excess goes to the swap and will slow down the entire system
|
||||||
|
|
||||||
|
MONO_VERSIONS=""
|
||||||
|
|
||||||
|
# Future versions
|
||||||
|
MONO_VERSIONS="$MONO_VERSIONS 6.10=preview-xenial"
|
||||||
|
|
||||||
|
# Semi-Supported versions
|
||||||
|
MONO_VERSIONS="$MONO_VERSIONS 6.8 6.6 6.4 6.0"
|
||||||
|
|
||||||
|
# Supported versions
|
||||||
|
MONO_VERSIONS="$MONO_VERSIONS 5.20 5.18"
|
||||||
|
|
||||||
|
# Legacy unsupported versions (but appear to work)
|
||||||
|
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12"
|
||||||
|
|
||||||
|
# Legacy unsupported versions
|
||||||
|
MONO_VERSIONS="$MONO_VERSIONS 5.10 5.8 5.4 5.0"
|
||||||
|
#MONO_VERSIONS="$MONO_VERSIONS 4.8=stable-wheezy/snapshots/4.8"
|
||||||
|
|
||||||
|
if [ "$opt_version" != "" ]; then
|
||||||
|
MONO_VERSIONS="$opt_version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ${PWD}/../../_tests_results
|
||||||
|
|
||||||
|
prepOne() {
|
||||||
|
local MONO_VERSION_PAIR=$1
|
||||||
|
|
||||||
|
MONO_VERSION_SPLIT=(${MONO_VERSION_PAIR//=/ })
|
||||||
|
MONO_VERSION=${MONO_VERSION_SPLIT[0]}
|
||||||
|
MONO_URL=${MONO_VERSION_SPLIT[1]:-"stable-xenial/snapshots/$MONO_VERSION"}
|
||||||
|
|
||||||
|
echo "Building Test Docker for mono $MONO_VERSION"
|
||||||
|
|
||||||
|
if [ "$opt_mode" != "sonarr" ]; then
|
||||||
|
docker build -t sonarr-test-$MONO_VERSION --build-arg MONO_VERSION=$MONO_VERSION --build-arg MONO_URL=$MONO_URL --file mono/complete/Dockerfile mono
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$opt_mode" != "complete" ] && [ "$MONO_VERSION" != "5.0" ]; then
|
||||||
|
docker build -t sonarr-test-$MONO_VERSION-sonarr --build-arg MONO_VERSION=$MONO_VERSION --build-arg MONO_URL=$MONO_URL --file mono/sonarr/Dockerfile mono
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
runOne() {
|
||||||
|
local MONO_VERSION_PAIR=$1
|
||||||
|
|
||||||
|
MONO_VERSION_SPLIT=(${MONO_VERSION_PAIR//=/ })
|
||||||
|
MONO_VERSION=${MONO_VERSION_SPLIT[0]}
|
||||||
|
|
||||||
|
echo "Running Test Docker for mono $MONO_VERSION"
|
||||||
|
|
||||||
|
if [ "$opt_mode" != "sonarr" ]; then
|
||||||
|
dockerArgs="--rm"
|
||||||
|
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_linux:/data/_tests_linux:ro"
|
||||||
|
dockerArgs="$dockerArgs -v /${PWD}/../../_output_linux:/data/_output_linux:ro"
|
||||||
|
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_results/mono-$MONO_VERSION:/data/_tests_results"
|
||||||
|
dockerArgs="$dockerArgs --mount type=tmpfs,destination=//data/test,tmpfs-size=1g"
|
||||||
|
docker run $dockerArgs sonarr-test-$MONO_VERSION
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$opt_mode" != "complete" ] && [ "$MONO_VERSION" != "5.0" ]; then
|
||||||
|
dockerArgs="--rm"
|
||||||
|
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_linux:/data/_tests_linux:ro"
|
||||||
|
dockerArgs="$dockerArgs -v /${PWD}/../../_output_linux:/data/_output_linux:ro"
|
||||||
|
dockerArgs="$dockerArgs -v /${PWD}/../../_tests_results/mono-$MONO_VERSION-sonarr:/data/_tests_results"
|
||||||
|
dockerArgs="$dockerArgs --mount type=tmpfs,destination=//data/test,tmpfs-size=1g"
|
||||||
|
docker run $dockerArgs sonarr-test-$MONO_VERSION-sonarr
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Finished Test Docker for mono $MONO_VERSION"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "$opt_report" != "1" ]; then
|
||||||
|
|
||||||
|
if [ "$opt_parallel" == "1" ]; then
|
||||||
|
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||||
|
prepOne "$MONO_VERSION_PAIR"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
|
||||||
|
if [ "$opt_parallel" == "1" ]; then
|
||||||
|
runOne "$MONO_VERSION_PAIR" &
|
||||||
|
else
|
||||||
|
prepOne "$MONO_VERSION_PAIR"
|
||||||
|
runOne "$MONO_VERSION_PAIR"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$opt_parallel" == "1" ]; then
|
||||||
|
echo "Waiting for all runs to finish"
|
||||||
|
wait
|
||||||
|
echo "Finished all runs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
grep "<test-run" ../../_tests_results/**/*.xml | sed -r 's/.*?mono-([0-9.]+(-s)?).*?_([IU]).*?\.xml.*?failed="([0-9]*)".*/\1\t\3:\tfailed \4/g' | sort -V -t.
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
module.exports = [
|
|
||||||
'>0.25%',
|
|
||||||
'not ie 11',
|
|
||||||
'not op_mini all',
|
|
||||||
'not chrome < 60'
|
|
||||||
];
|
|
||||||
@@ -10,8 +10,7 @@ gulp.task('build',
|
|||||||
'webpack',
|
'webpack',
|
||||||
'copyHtml',
|
'copyHtml',
|
||||||
'copyFonts',
|
'copyFonts',
|
||||||
'copyImages',
|
'copyImages'
|
||||||
'copyJs'
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,17 +5,6 @@ const cache = require('gulp-cached');
|
|||||||
const livereload = require('gulp-livereload');
|
const livereload = require('gulp-livereload');
|
||||||
const paths = require('./helpers/paths.js');
|
const paths = require('./helpers/paths.js');
|
||||||
|
|
||||||
gulp.task('copyJs', () => {
|
|
||||||
return gulp.src(
|
|
||||||
[
|
|
||||||
path.join(paths.src.root, 'polyfills.js')
|
|
||||||
], { base: paths.src.root })
|
|
||||||
.pipe(cache('copyJs'))
|
|
||||||
.pipe(print())
|
|
||||||
.pipe(gulp.dest(paths.dest.root))
|
|
||||||
.pipe(livereload());
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task('copyHtml', () => {
|
gulp.task('copyHtml', () => {
|
||||||
return gulp.src(paths.src.html, { base: paths.src.root })
|
return gulp.src(paths.src.html, { base: paths.src.root })
|
||||||
.pipe(cache('copyHtml'))
|
.pipe(cache('copyHtml'))
|
||||||
|
|||||||
@@ -5,15 +5,22 @@ const path = require('path');
|
|||||||
const webpack = require('webpack');
|
const webpack = require('webpack');
|
||||||
const errorHandler = require('./helpers/errorHandler');
|
const errorHandler = require('./helpers/errorHandler');
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const browsers = require('../browsers');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
const uiFolder = 'UI';
|
const uiFolder = 'UI';
|
||||||
const frontendFolder = path.join(__dirname, '..');
|
const frontendFolder = path.join(__dirname, '..');
|
||||||
const srcFolder = path.join(frontendFolder, 'src');
|
const srcFolder = path.join(frontendFolder, 'src');
|
||||||
const isProduction = process.argv.indexOf('--production') > -1;
|
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('Source Folder:', srcFolder);
|
||||||
|
console.log('Output Folder:', distFolder);
|
||||||
console.log('isProduction:', isProduction);
|
console.log('isProduction:', isProduction);
|
||||||
|
console.log('isProfiling:', isProfiling);
|
||||||
|
|
||||||
const cssVarsFiles = [
|
const cssVarsFiles = [
|
||||||
'../src/Styles/Variables/colors',
|
'../src/Styles/Variables/colors',
|
||||||
@@ -23,6 +30,22 @@ const cssVarsFiles = [
|
|||||||
'../src/Styles/Variables/zIndexes'
|
'../src/Styles/Variables/zIndexes'
|
||||||
].map(require.resolve);
|
].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 = [
|
const plugins = [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
__DEV__: !isProduction,
|
__DEV__: !isProduction,
|
||||||
@@ -30,7 +53,12 @@ const plugins = [
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
new MiniCssExtractPlugin({
|
new MiniCssExtractPlugin({
|
||||||
filename: path.join('_output', uiFolder, 'Content', 'styles.css')
|
filename: path.join('Content', 'styles.css')
|
||||||
|
}),
|
||||||
|
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
template: 'frontend/src/index.html',
|
||||||
|
filename: 'index.html'
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -47,8 +75,6 @@ const config = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
entry: {
|
entry: {
|
||||||
preload: 'preload.js',
|
|
||||||
vendor: 'vendor.js',
|
|
||||||
index: 'index.js'
|
index: 'index.js'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -64,12 +90,20 @@ const config = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
output: {
|
output: {
|
||||||
filename: path.join('_output', uiFolder, '[name].js'),
|
path: distFolder,
|
||||||
|
filename: '[name].js',
|
||||||
sourceMapFilename: '[file].map'
|
sourceMapFilename: '[file].map'
|
||||||
},
|
},
|
||||||
|
|
||||||
optimization: {
|
optimization: {
|
||||||
chunkIds: 'named'
|
chunkIds: 'named',
|
||||||
|
splitChunks: {
|
||||||
|
chunks: 'initial'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
performance: {
|
||||||
|
hints: false
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins,
|
plugins,
|
||||||
@@ -83,6 +117,22 @@ const config = {
|
|||||||
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.worker\.js$/,
|
||||||
|
issuer: {
|
||||||
|
// monaco-editor includes the editor.worker.js in other language workers,
|
||||||
|
// don't use worker-loader in that case
|
||||||
|
exclude: /monaco-editor/
|
||||||
|
},
|
||||||
|
use: {
|
||||||
|
loader: 'worker-loader',
|
||||||
|
options: {
|
||||||
|
name: '[name].js',
|
||||||
|
inline: inlineWebWorkers,
|
||||||
|
fallback: !inlineWebWorkers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
test: /\.js?$/,
|
test: /\.js?$/,
|
||||||
exclude: /(node_modules|JsLibraries)/,
|
exclude: /(node_modules|JsLibraries)/,
|
||||||
@@ -100,7 +150,7 @@ const config = {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
targets: browsers
|
corejs: 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -119,8 +169,9 @@ const config = {
|
|||||||
loader: 'css-loader',
|
loader: 'css-loader',
|
||||||
options: {
|
options: {
|
||||||
importLoaders: 1,
|
importLoaders: 1,
|
||||||
localIdentName: '[name]/[local]/[hash:base64:5]',
|
modules: {
|
||||||
modules: true
|
localIdentName: '[name]/[local]/[hash:base64:5]'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -182,9 +233,27 @@ const config = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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', () => {
|
gulp.task('webpack', () => {
|
||||||
return webpackStream(config)
|
return webpackStream(config)
|
||||||
.pipe(gulp.dest('./'));
|
.pipe(gulp.dest('_output/UI'));
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('webpackWatch', () => {
|
gulp.task('webpackWatch', () => {
|
||||||
@@ -192,7 +261,7 @@ gulp.task('webpackWatch', () => {
|
|||||||
|
|
||||||
return webpackStream(config)
|
return webpackStream(config)
|
||||||
.on('error', errorHandler)
|
.on('error', errorHandler)
|
||||||
.pipe(gulp.dest('./'))
|
.pipe(gulp.dest('_output/UI'))
|
||||||
.on('error', errorHandler)
|
.on('error', errorHandler)
|
||||||
.pipe(livereload())
|
.pipe(livereload())
|
||||||
.on('error', errorHandler);
|
.on('error', errorHandler);
|
||||||
|
|||||||
20
frontend/jsconfig.json
Normal file
20
frontend/jsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es6",
|
||||||
|
"checkJs": false,
|
||||||
|
"baseUrl": "src",
|
||||||
|
"jsx": "react",
|
||||||
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"paths": {
|
||||||
|
"*": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src/**/*"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
const reload = require('require-nocache')(module);
|
const reload = require('require-nocache')(module);
|
||||||
const browsers = require('./browsers');
|
|
||||||
|
|
||||||
module.exports = (ctx, configPath, options) => {
|
module.exports = (ctx, configPath, options) => {
|
||||||
const config = {
|
const config = {
|
||||||
@@ -16,10 +15,7 @@ module.exports = (ctx, configPath, options) => {
|
|||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
'postcss-color-function': {},
|
'postcss-color-function': {},
|
||||||
'postcss-nested': {},
|
'postcss-nested': {}
|
||||||
autoprefixer: {
|
|
||||||
browsers
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import TableBody from 'Components/Table/TableBody';
|
|||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
@@ -56,7 +56,7 @@ class Blacklist extends Component {
|
|||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
<PageContentBodyConnector>
|
<PageContentBody>
|
||||||
{
|
{
|
||||||
isFetching && !isPopulated &&
|
isFetching && !isPopulated &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
@@ -103,7 +103,7 @@ class Blacklist extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</PageContentBodyConnector>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,6 +232,30 @@ function HistoryDetails(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadIgnored') {
|
||||||
|
const {
|
||||||
|
message
|
||||||
|
} = data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Name"
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!!message &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Message"
|
||||||
|
data={message}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ function getHeaderTitle(eventType) {
|
|||||||
return 'Episode File Deleted';
|
return 'Episode File Deleted';
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return 'Episode File Renamed';
|
return 'Episode File Renamed';
|
||||||
|
case 'downloadIgnored':
|
||||||
|
return 'Download Ignored';
|
||||||
default:
|
default:
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import TableBody from 'Components/Table/TableBody';
|
|||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
@@ -96,7 +96,7 @@ class History extends Component {
|
|||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
<PageContentBodyConnector>
|
<PageContentBody>
|
||||||
{
|
{
|
||||||
isFetchingAny && !isAllPopulated &&
|
isFetchingAny && !isAllPopulated &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
@@ -147,7 +147,7 @@ class History extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</PageContentBodyConnector>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ function getIconName(eventType) {
|
|||||||
return icons.DELETE;
|
return icons.DELETE;
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
|
case 'downloadIgnored':
|
||||||
|
return icons.IGNORE;
|
||||||
default:
|
default:
|
||||||
return icons.UNKNOWN;
|
return icons.UNKNOWN;
|
||||||
}
|
}
|
||||||
@@ -47,6 +49,8 @@ function getTooltip(eventType, data) {
|
|||||||
return 'Episode file deleted';
|
return 'Episode file deleted';
|
||||||
case 'episodeFileRenamed':
|
case 'episodeFileRenamed':
|
||||||
return 'Episode file renamed';
|
return 'Episode file renamed';
|
||||||
|
case 'downloadIgnored':
|
||||||
|
return 'Episode Download Ignored';
|
||||||
default:
|
default:
|
||||||
return 'Unknown event';
|
return 'Unknown event';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
@@ -12,7 +13,7 @@ import Table from 'Components/Table/Table';
|
|||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
@@ -36,34 +37,26 @@ class Queue extends Component {
|
|||||||
lastToggled: null,
|
lastToggled: null,
|
||||||
selectedState: {},
|
selectedState: {},
|
||||||
isPendingSelected: false,
|
isPendingSelected: false,
|
||||||
isConfirmRemoveModalOpen: false
|
isConfirmRemoveModalOpen: false,
|
||||||
|
items: props.items
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps) {
|
componentDidUpdate(prevProps) {
|
||||||
// Don't update when fetching has completed if items have changed,
|
const {
|
||||||
// before episodes start fetching or when episodes start fetching.
|
items,
|
||||||
|
isEpisodesFetching
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.props.isFetching &&
|
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
|
||||||
nextProps.isPopulated &&
|
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
|
||||||
hasDifferentItems(this.props.items, nextProps.items) &&
|
|
||||||
nextProps.items.some((e) => e.episodeId)
|
|
||||||
) {
|
) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
|
||||||
this.setState((state) => {
|
this.setState((state) => {
|
||||||
return removeOldSelectedState(state, prevProps.items);
|
return {
|
||||||
|
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||||
|
items
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -71,7 +64,7 @@ class Queue extends Component {
|
|||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
const selectedIds = this.getSelectedIds();
|
||||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
|
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||||
@@ -107,8 +100,8 @@ class Queue extends Component {
|
|||||||
this.setState({ isConfirmRemoveModalOpen: true });
|
this.setState({ isConfirmRemoveModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveSelectedConfirmed = (blacklist) => {
|
onRemoveSelectedConfirmed = (payload) => {
|
||||||
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
|
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +117,6 @@ class Queue extends Component {
|
|||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
|
||||||
isEpisodesFetching,
|
isEpisodesFetching,
|
||||||
isEpisodesPopulated,
|
isEpisodesPopulated,
|
||||||
episodesError,
|
episodesError,
|
||||||
@@ -132,7 +124,7 @@ class Queue extends Component {
|
|||||||
totalRecords,
|
totalRecords,
|
||||||
isGrabbing,
|
isGrabbing,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isCheckForFinishedDownloadExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -142,13 +134,15 @@ class Queue extends Component {
|
|||||||
allUnselected,
|
allUnselected,
|
||||||
selectedState,
|
selectedState,
|
||||||
isConfirmRemoveModalOpen,
|
isConfirmRemoveModalOpen,
|
||||||
isPendingSelected
|
isPendingSelected,
|
||||||
|
items
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
|
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
||||||
const hasError = error || episodesError;
|
const hasError = error || episodesError;
|
||||||
const selectedCount = this.getSelectedIds().length;
|
const selectedIds = this.getSelectedIds();
|
||||||
|
const selectedCount = selectedIds.length;
|
||||||
const disableSelectedActions = selectedCount === 0;
|
const disableSelectedActions = selectedCount === 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -197,7 +191,7 @@ class Queue extends Component {
|
|||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
<PageContentBodyConnector>
|
<PageContentBody>
|
||||||
{
|
{
|
||||||
isRefreshing && !isAllPopulated &&
|
isRefreshing && !isAllPopulated &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
@@ -254,11 +248,18 @@ class Queue extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</PageContentBodyConnector>
|
</PageContentBody>
|
||||||
|
|
||||||
<RemoveQueueItemsModal
|
<RemoveQueueItemsModal
|
||||||
isOpen={isConfirmRemoveModalOpen}
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
selectedCount={selectedCount}
|
selectedCount={selectedCount}
|
||||||
|
canIgnore={isConfirmRemoveModalOpen && (
|
||||||
|
selectedIds.every((id) => {
|
||||||
|
const item = items.find((i) => i.id === id);
|
||||||
|
|
||||||
|
return !!(item && item.seriesId && item.episodeId);
|
||||||
|
})
|
||||||
|
)}
|
||||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||||
onModalClose={this.onConfirmRemoveModalClose}
|
onModalClose={this.onConfirmRemoveModalClose}
|
||||||
/>
|
/>
|
||||||
@@ -279,7 +280,7 @@ Queue.propTypes = {
|
|||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
|
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ function createMapStateToProps() {
|
|||||||
(state) => state.episodes,
|
(state) => state.episodes,
|
||||||
(state) => state.queue.options,
|
(state) => state.queue.options,
|
||||||
(state) => state.queue.paged,
|
(state) => state.queue.paged,
|
||||||
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||||
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => {
|
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||||
return {
|
return {
|
||||||
isEpisodesFetching: episodes.isFetching,
|
isEpisodesFetching: episodes.isFetching,
|
||||||
isEpisodesPopulated: episodes.isPopulated,
|
isEpisodesPopulated: episodes.isPopulated,
|
||||||
episodesError: episodes.error,
|
episodesError: episodes.error,
|
||||||
isCheckForFinishedDownloadExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
...options,
|
...options,
|
||||||
...queue
|
...queue
|
||||||
};
|
};
|
||||||
@@ -129,7 +129,7 @@ class QueueConnector extends Component {
|
|||||||
|
|
||||||
onRefreshPress = () => {
|
onRefreshPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
|
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,8 +137,8 @@ class QueueConnector extends Component {
|
|||||||
this.props.grabQueueItems({ ids });
|
this.props.grabQueueItems({ ids });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveSelectedPress = (ids, blacklist) => {
|
onRemoveSelectedPress = (payload) => {
|
||||||
this.props.removeQueueItems({ ids, blacklist });
|
this.props.removeQueueItems(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ function QueueDetails(props) {
|
|||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
estimatedCompletionTime,
|
estimatedCompletionTime,
|
||||||
status: queueStatus,
|
status,
|
||||||
|
trackedDownloadState,
|
||||||
|
trackedDownloadStatus,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
progressBar
|
progressBar
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const status = queueStatus.toLowerCase();
|
|
||||||
|
|
||||||
const progress = (100 - sizeleft / size * 100);
|
const progress = (100 - sizeleft / size * 100);
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (status === 'pending') {
|
||||||
@@ -39,7 +39,35 @@ function QueueDetails(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: show an icon when download is complete, but not imported yet?
|
if (trackedDownloadStatus === 'warning') {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={'Downloaded - Unable to Import: check logs for details'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importPending') {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={kinds.PURPLE}
|
||||||
|
title={'Downloaded - Waiting to Import'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importing') {
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name={icons.DOWNLOAD}
|
||||||
|
kind={kinds.PURPLE}
|
||||||
|
title={'Downloaded - Importing'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorMessage) {
|
if (errorMessage) {
|
||||||
@@ -90,6 +118,8 @@ QueueDetails.propTypes = {
|
|||||||
sizeleft: PropTypes.number.isRequired,
|
sizeleft: PropTypes.number.isRequired,
|
||||||
estimatedCompletionTime: PropTypes.string,
|
estimatedCompletionTime: PropTypes.string,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadState: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
progressBar: PropTypes.node.isRequired
|
progressBar: PropTypes.node.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class QueueRow extends Component {
|
|||||||
title,
|
title,
|
||||||
status,
|
status,
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
statusMessages,
|
statusMessages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
series,
|
series,
|
||||||
@@ -100,8 +101,8 @@ class QueueRow extends Component {
|
|||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const progress = 100 - (sizeleft / size * 100);
|
const progress = 100 - (sizeleft / size * 100);
|
||||||
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
|
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||||
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
|
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -129,6 +130,7 @@ class QueueRow extends Component {
|
|||||||
sourceTitle={title}
|
sourceTitle={title}
|
||||||
status={status}
|
status={status}
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
statusMessages={statusMessages}
|
statusMessages={statusMessages}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
/>
|
/>
|
||||||
@@ -220,9 +222,13 @@ class QueueRow extends Component {
|
|||||||
if (name === 'quality') {
|
if (name === 'quality') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<EpisodeQuality
|
{
|
||||||
quality={quality}
|
quality ?
|
||||||
/>
|
<EpisodeQuality
|
||||||
|
quality={quality}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -350,6 +356,7 @@ class QueueRow extends Component {
|
|||||||
<RemoveQueueItemModal
|
<RemoveQueueItemModal
|
||||||
isOpen={isRemoveQueueItemModalOpen}
|
isOpen={isRemoveQueueItemModalOpen}
|
||||||
sourceTitle={title}
|
sourceTitle={title}
|
||||||
|
canIgnore={!!series}
|
||||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||||
onModalClose={this.onRemoveQueueItemModalClose}
|
onModalClose={this.onRemoveQueueItemModalClose}
|
||||||
/>
|
/>
|
||||||
@@ -365,6 +372,7 @@ QueueRow.propTypes = {
|
|||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
trackedDownloadStatus: PropTypes.string,
|
trackedDownloadStatus: PropTypes.string,
|
||||||
|
trackedDownloadState: PropTypes.string,
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
series: PropTypes.object,
|
series: PropTypes.object,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -15,11 +14,11 @@ function createMapStateToProps() {
|
|||||||
createEpisodeSelector(),
|
createEpisodeSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
(series, episode, uiSettings) => {
|
(series, episode, uiSettings) => {
|
||||||
const result = _.pick(uiSettings, [
|
const result = {
|
||||||
'showRelativeDates',
|
showRelativeDates: uiSettings.showRelativeDates,
|
||||||
'shortDateFormat',
|
shortDateFormat: uiSettings.shortDateFormat,
|
||||||
'timeFormat'
|
timeFormat: uiSettings.timeFormat
|
||||||
]);
|
};
|
||||||
|
|
||||||
result.series = series;
|
result.series = series;
|
||||||
result.episode = episode;
|
result.episode = episode;
|
||||||
@@ -43,8 +42,8 @@ class QueueRowConnector extends Component {
|
|||||||
this.props.grabQueueItem({ id: this.props.id });
|
this.props.grabQueueItem({ id: this.props.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveQueueItemPress = (blacklist) => {
|
onRemoveQueueItemPress = (payload) => {
|
||||||
this.props.removeQueueItem({ id: this.props.id, blacklist });
|
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -37,63 +37,79 @@ function QueueStatusCell(props) {
|
|||||||
const {
|
const {
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
status,
|
status,
|
||||||
trackedDownloadStatus = 'Ok',
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
statusMessages,
|
statusMessages,
|
||||||
errorMessage
|
errorMessage
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasWarning = trackedDownloadStatus === 'Warning';
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
const hasError = trackedDownloadStatus === 'Error';
|
const hasError = trackedDownloadStatus === 'error';
|
||||||
|
|
||||||
// status === 'downloading'
|
// status === 'downloading'
|
||||||
let iconName = icons.DOWNLOADING;
|
let iconName = icons.DOWNLOADING;
|
||||||
let iconKind = kinds.DEFAULT;
|
let iconKind = kinds.DEFAULT;
|
||||||
let title = 'Downloading';
|
let title = 'Downloading';
|
||||||
|
|
||||||
if (hasWarning) {
|
if (status === 'paused') {
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'Paused') {
|
|
||||||
iconName = icons.PAUSED;
|
iconName = icons.PAUSED;
|
||||||
title = 'Paused';
|
title = 'Paused';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'Queued') {
|
if (status === 'queued') {
|
||||||
iconName = icons.QUEUED;
|
iconName = icons.QUEUED;
|
||||||
title = 'Queued';
|
title = 'Queued';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'Completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOADED;
|
iconName = icons.DOWNLOADED;
|
||||||
title = 'Downloaded';
|
title = 'Downloaded';
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importPending') {
|
||||||
|
title += ' - Waiting to Import';
|
||||||
|
iconKind = kinds.PURPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importing') {
|
||||||
|
title += ' - Importing';
|
||||||
|
iconKind = kinds.PURPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'failedPending') {
|
||||||
|
title += ' - Waiting to Process';
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'Delay') {
|
if (hasWarning) {
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'delay') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
title = 'Pending';
|
title = 'Pending';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'DownloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
iconName = icons.PENDING;
|
iconName = icons.PENDING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = 'Pending - Download client is unavailable';
|
title = 'Pending - Download client is unavailable';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'Failed') {
|
if (status === 'failed') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = 'Download failed';
|
title = 'Download failed';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'Warning') {
|
if (status === 'warning') {
|
||||||
iconName = icons.DOWNLOADING;
|
iconName = icons.DOWNLOADING;
|
||||||
iconKind = kinds.WARNING;
|
iconKind = kinds.WARNING;
|
||||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
if (status === 'Completed') {
|
if (status === 'completed') {
|
||||||
iconName = icons.DOWNLOAD;
|
iconName = icons.DOWNLOAD;
|
||||||
iconKind = kinds.DANGER;
|
iconKind = kinds.DANGER;
|
||||||
title = `Import failed: ${sourceTitle}`;
|
title = `Import failed: ${sourceTitle}`;
|
||||||
@@ -125,9 +141,15 @@ function QueueStatusCell(props) {
|
|||||||
QueueStatusCell.propTypes = {
|
QueueStatusCell.propTypes = {
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
trackedDownloadStatus: PropTypes.string,
|
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadState: PropTypes.string.isRequired,
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
errorMessage: PropTypes.string
|
errorMessage: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QueueStatusCell.defaultProps = {
|
||||||
|
trackedDownloadStatus: 'Ok',
|
||||||
|
trackedDownloadState: 'Downloading'
|
||||||
|
};
|
||||||
|
|
||||||
export default QueueStatusCell;
|
export default QueueStatusCell;
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
.message {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import styles from './RemoveQueueItemModal.css';
|
|
||||||
|
|
||||||
class RemoveQueueItemModal extends Component {
|
class RemoveQueueItemModal extends Component {
|
||||||
|
|
||||||
@@ -21,26 +20,41 @@ class RemoveQueueItemModal extends Component {
|
|||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
remove: true,
|
||||||
blacklist: false
|
blacklist: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
resetState = function() {
|
||||||
|
this.setState({
|
||||||
|
remove: true,
|
||||||
|
blacklist: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
onRemoveChange = ({ value }) => {
|
||||||
|
this.setState({ remove: value });
|
||||||
|
}
|
||||||
|
|
||||||
onBlacklistChange = ({ value }) => {
|
onBlacklistChange = ({ value }) => {
|
||||||
this.setState({ blacklist: value });
|
this.setState({ blacklist: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveQueueItemConfirmed = () => {
|
onRemoveConfirmed = () => {
|
||||||
const blacklist = this.state.blacklist;
|
const state = this.state;
|
||||||
|
|
||||||
this.setState({ blacklist: false });
|
this.resetState();
|
||||||
this.props.onRemovePress(blacklist);
|
this.props.onRemovePress(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
onModalClose = () => {
|
onModalClose = () => {
|
||||||
this.setState({ blacklist: false });
|
this.resetState();
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +64,11 @@ class RemoveQueueItemModal extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
sourceTitle
|
sourceTitle,
|
||||||
|
canIgnore
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const blacklist = this.state.blacklist;
|
const { remove, blacklist } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -69,17 +84,31 @@ class RemoveQueueItemModal extends Component {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.message}>
|
<div>
|
||||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>Remove From Download Client</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="remove"
|
||||||
|
value={remove}
|
||||||
|
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||||
|
isDisabled={!canIgnore}
|
||||||
|
onChange={this.onRemoveChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Blacklist Release</FormLabel>
|
<FormLabel>Blacklist Release</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="blacklist"
|
name="blacklist"
|
||||||
value={blacklist}
|
value={blacklist}
|
||||||
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
|
||||||
onChange={this.onBlacklistChange}
|
onChange={this.onBlacklistChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -93,7 +122,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onRemoveQueueItemConfirmed}
|
onPress={this.onRemoveConfirmed}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
@@ -107,6 +136,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
RemoveQueueItemModal.propTypes = {
|
RemoveQueueItemModal.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
canIgnore: PropTypes.bool.isRequired,
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
onRemovePress: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,26 +21,41 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
remove: true,
|
||||||
blacklist: false
|
blacklist: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Control
|
||||||
|
|
||||||
|
resetState = function() {
|
||||||
|
this.setState({
|
||||||
|
remove: true,
|
||||||
|
blacklist: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onRemoveChange = ({ value }) => {
|
||||||
|
this.setState({ remove: value });
|
||||||
|
}
|
||||||
|
|
||||||
onBlacklistChange = ({ value }) => {
|
onBlacklistChange = ({ value }) => {
|
||||||
this.setState({ blacklist: value });
|
this.setState({ blacklist: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveQueueItemConfirmed = () => {
|
onRemoveConfirmed = () => {
|
||||||
const blacklist = this.state.blacklist;
|
const state = this.state;
|
||||||
|
|
||||||
this.setState({ blacklist: false });
|
this.resetState();
|
||||||
this.props.onRemovePress(blacklist);
|
this.props.onRemovePress(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
onModalClose = () => {
|
onModalClose = () => {
|
||||||
this.setState({ blacklist: false });
|
this.resetState();
|
||||||
this.props.onModalClose();
|
this.props.onModalClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +65,11 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
selectedCount
|
selectedCount,
|
||||||
|
canIgnore
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const blacklist = this.state.blacklist;
|
const { remove, blacklist } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -74,7 +90,23 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Blacklist Release</FormLabel>
|
<FormLabel>Remove From Download Client</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="remove"
|
||||||
|
value={remove}
|
||||||
|
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||||
|
isDisabled={!canIgnore}
|
||||||
|
onChange={this.onRemoveChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>
|
||||||
|
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="blacklist"
|
name="blacklist"
|
||||||
@@ -93,7 +125,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onRemoveQueueItemConfirmed}
|
onPress={this.onRemoveConfirmed}
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</Button>
|
</Button>
|
||||||
@@ -107,6 +139,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
RemoveQueueItemsModal.propTypes = {
|
RemoveQueueItemsModal.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
selectedCount: PropTypes.number.isRequired,
|
selectedCount: PropTypes.number.isRequired,
|
||||||
|
canIgnore: PropTypes.bool.isRequired,
|
||||||
onRemovePress: PropTypes.func.isRequired,
|
onRemovePress: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ function TimeleftCell(props) {
|
|||||||
timeFormat
|
timeFormat
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (status === 'Delay') {
|
if (status === 'delay') {
|
||||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ function TimeleftCell(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'DownloadClientUnavailable') {
|
if (status === 'downloadClientUnavailable') {
|
||||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ function TimeleftCell(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!timeleft) {
|
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.timeleft}>
|
<TableRowCell className={styles.timeleft}>
|
||||||
-
|
-
|
||||||
|
|||||||
@@ -35,14 +35,20 @@
|
|||||||
.message {
|
.message {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: $largeFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.helpText {
|
.helpText {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: 300;
|
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noSeriesText {
|
||||||
|
margin-top: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.noResults {
|
.noResults {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
||||||
import styles from './AddNewSeries.css';
|
import styles from './AddNewSeries.css';
|
||||||
|
|
||||||
@@ -78,7 +79,8 @@ class AddNewSeries extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
items
|
items,
|
||||||
|
hasExistingSeries
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const term = this.state.term;
|
const term = this.state.term;
|
||||||
@@ -86,7 +88,7 @@ class AddNewSeries extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title="Add New Series">
|
<PageContent title="Add New Series">
|
||||||
<PageContentBodyConnector>
|
<PageContentBody>
|
||||||
<div className={styles.searchContainer}>
|
<div className={styles.searchContainer}>
|
||||||
<div className={styles.searchIconContainer}>
|
<div className={styles.searchIconContainer}>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -121,8 +123,13 @@ class AddNewSeries extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error ?
|
||||||
<div>Failed to load search results, please try again.</div>
|
<div className={styles.message}>
|
||||||
|
<div className={styles.helpText}>
|
||||||
|
Failed to load search results, please try again.
|
||||||
|
</div>
|
||||||
|
<div>{getErrorMessage(error)}</div>
|
||||||
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -155,15 +162,36 @@ class AddNewSeries extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!term &&
|
term ?
|
||||||
|
null :
|
||||||
<div className={styles.message}>
|
<div className={styles.message}>
|
||||||
<div className={styles.helpText}>It's easy to add a new series, just start typing the name the series you want to add.</div>
|
<div className={styles.helpText}>
|
||||||
|
It's easy to add a new series, just start typing the name the series you want to add.
|
||||||
|
</div>
|
||||||
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
|
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!term && !hasExistingSeries ?
|
||||||
|
<div className={styles.message}>
|
||||||
|
<div className={styles.noSeriesText}>
|
||||||
|
You haven't added any series yet, do you want to import some or all of your series first?
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
to="/add/import"
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
>
|
||||||
|
Import Existing Series
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</PageContentBodyConnector>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -176,6 +204,7 @@ AddNewSeries.propTypes = {
|
|||||||
isAdding: PropTypes.bool.isRequired,
|
isAdding: PropTypes.bool.isRequired,
|
||||||
addError: PropTypes.object,
|
addError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
hasExistingSeries: PropTypes.bool.isRequired,
|
||||||
onSeriesLookupChange: PropTypes.func.isRequired,
|
onSeriesLookupChange: PropTypes.func.isRequired,
|
||||||
onClearSeriesLookup: PropTypes.func.isRequired
|
onClearSeriesLookup: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import AddNewSeries from './AddNewSeries';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.addSeries,
|
(state) => state.addSeries,
|
||||||
|
(state) => state.series.items.length,
|
||||||
(state) => state.router.location,
|
(state) => state.router.location,
|
||||||
(addSeries, location) => {
|
(addSeries, existingSeriesCount, location) => {
|
||||||
const { params } = parseUrl(location.search);
|
const { params } = parseUrl(location.search);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
...addSeries,
|
||||||
term: params.term,
|
term: params.term,
|
||||||
...addSeries
|
hasExistingSeries: existingSeriesCount > 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import ModalBody from 'Components/Modal/ModalBody';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import SeriesPoster from 'Series/SeriesPoster';
|
import SeriesPoster from 'Series/SeriesPoster';
|
||||||
|
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||||
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||||
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||||
import styles from './AddNewSeriesModalContent.css';
|
import styles from './AddNewSeriesModalContent.css';
|
||||||
@@ -27,10 +28,19 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
|
||||||
|
props.seriesType.value :
|
||||||
|
props.initialSeriesType,
|
||||||
searchForMissingEpisodes: false
|
searchForMissingEpisodes: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
if (this.props.seriesType.value !== prevProps.seriesType.value) {
|
||||||
|
this.setState({ seriesType: this.props.seriesType.value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
@@ -47,7 +57,12 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAddSeriesPress = () => {
|
onAddSeriesPress = () => {
|
||||||
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
|
const {
|
||||||
|
searchForMissingEpisodes,
|
||||||
|
seriesType
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
this.props.onAddSeriesPress(searchForMissingEpisodes, seriesType);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -66,9 +81,11 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
languageProfileId,
|
languageProfileId,
|
||||||
seriesType,
|
seriesType,
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
|
folder,
|
||||||
tags,
|
tags,
|
||||||
showLanguageProfile,
|
showLanguageProfile,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
|
isWindows,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onInputChange,
|
onInputChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
@@ -115,6 +132,15 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.ROOT_FOLDER_SELECT}
|
type={inputTypes.ROOT_FOLDER_SELECT}
|
||||||
name="rootFolderPath"
|
name="rootFolderPath"
|
||||||
|
valueOptions={{
|
||||||
|
seriesFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
selectedValueOptions={{
|
||||||
|
seriesFolder: folder,
|
||||||
|
isWindows
|
||||||
|
}}
|
||||||
|
helpText={`'${folder}' subfolder will be created automatically`}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...rootFolderPath}
|
{...rootFolderPath}
|
||||||
/>
|
/>
|
||||||
@@ -189,6 +215,7 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
name="seriesType"
|
name="seriesType"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...seriesType}
|
{...seriesType}
|
||||||
|
value={this.state.seriesType}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
@@ -251,6 +278,7 @@ AddNewSeriesModalContent.propTypes = {
|
|||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
|
initialSeriesType: PropTypes.string.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isAdding: PropTypes.bool.isRequired,
|
isAdding: PropTypes.bool.isRequired,
|
||||||
addError: PropTypes.object,
|
addError: PropTypes.object,
|
||||||
@@ -260,9 +288,11 @@ AddNewSeriesModalContent.propTypes = {
|
|||||||
languageProfileId: PropTypes.object,
|
languageProfileId: PropTypes.object,
|
||||||
seriesType: PropTypes.object.isRequired,
|
seriesType: PropTypes.object.isRequired,
|
||||||
seasonFolder: PropTypes.object.isRequired,
|
seasonFolder: PropTypes.object.isRequired,
|
||||||
|
folder: PropTypes.string.isRequired,
|
||||||
tags: PropTypes.object.isRequired,
|
tags: PropTypes.object.isRequired,
|
||||||
showLanguageProfile: PropTypes.bool.isRequired,
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
isWindows: PropTypes.bool.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onAddSeriesPress: PropTypes.func.isRequired
|
onAddSeriesPress: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { setAddSeriesDefault, addSeries } from 'Store/Actions/addSeriesActions';
|
import { setAddSeriesDefault, addSeries } from 'Store/Actions/addSeriesActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import AddNewSeriesModalContent from './AddNewSeriesModalContent';
|
import AddNewSeriesModalContent from './AddNewSeriesModalContent';
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ function createMapStateToProps() {
|
|||||||
(state) => state.addSeries,
|
(state) => state.addSeries,
|
||||||
(state) => state.settings.languageProfiles,
|
(state) => state.settings.languageProfiles,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(addSeriesState, languageProfiles, dimensions) => {
|
createSystemStatusSelector(),
|
||||||
|
(addSeriesState, languageProfiles, dimensions, systemStatus) => {
|
||||||
const {
|
const {
|
||||||
isAdding,
|
isAdding,
|
||||||
addError,
|
addError,
|
||||||
@@ -32,6 +34,7 @@ function createMapStateToProps() {
|
|||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
validationWarnings,
|
validationWarnings,
|
||||||
|
isWindows: systemStatus.isWindows,
|
||||||
...settings
|
...settings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -52,14 +55,13 @@ class AddNewSeriesModalContentConnector extends Component {
|
|||||||
this.props.setAddSeriesDefault({ [name]: value });
|
this.props.setAddSeriesDefault({ [name]: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddSeriesPress = (searchForMissingEpisodes) => {
|
onAddSeriesPress = (searchForMissingEpisodes, seriesType) => {
|
||||||
const {
|
const {
|
||||||
tvdbId,
|
tvdbId,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
languageProfileId,
|
languageProfileId,
|
||||||
seriesType,
|
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
tags
|
tags
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -70,7 +72,7 @@ class AddNewSeriesModalContentConnector extends Component {
|
|||||||
monitor: monitor.value,
|
monitor: monitor.value,
|
||||||
qualityProfileId: qualityProfileId.value,
|
qualityProfileId: qualityProfileId.value,
|
||||||
languageProfileId: languageProfileId.value,
|
languageProfileId: languageProfileId.value,
|
||||||
seriesType: seriesType.value,
|
seriesType,
|
||||||
seasonFolder: seasonFolder.value,
|
seasonFolder: seasonFolder.value,
|
||||||
tags: tags.value,
|
tags: tags.value,
|
||||||
searchForMissingEpisodes
|
searchForMissingEpisodes
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
.searchResult {
|
.searchResult {
|
||||||
display: flex;
|
position: relative;
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: $white;
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.underlay {
|
||||||
|
@add-mixin cover;
|
||||||
|
|
||||||
|
background-color: $white;
|
||||||
transition: background 500ms;
|
transition: background 500ms;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
@@ -14,13 +19,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
@add-mixin linkOverlay;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.poster {
|
.poster {
|
||||||
flex: 0 0 170px;
|
flex: 0 0 170px;
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
height: 250px;
|
height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex: 0 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
|
display: flex;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
}
|
}
|
||||||
@@ -30,9 +47,22 @@
|
|||||||
color: $disabledColor;
|
color: $disabledColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tvdbLink {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
margin-top: -4px;
|
||||||
|
margin-left: auto;
|
||||||
|
color: $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tvdbLinkIcon {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.alreadyExistsIcon {
|
.alreadyExistsIcon {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: #37bc9b;
|
color: #37bc9b;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
|
if (!prevProps.isExistingSeries && this.props.isExistingSeries) {
|
||||||
this.onAddSerisModalClose();
|
this.onAddSeriesModalClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +35,14 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
this.setState({ isNewAddSeriesModalOpen: true });
|
this.setState({ isNewAddSeriesModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddSerisModalClose = () => {
|
onAddSeriesModalClose = () => {
|
||||||
this.setState({ isNewAddSeriesModalOpen: false });
|
this.setState({ isNewAddSeriesModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTVDBLinkPress = (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -53,6 +57,8 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
overview,
|
overview,
|
||||||
statistics,
|
statistics,
|
||||||
ratings,
|
ratings,
|
||||||
|
folder,
|
||||||
|
seriesType,
|
||||||
images,
|
images,
|
||||||
isExistingSeries,
|
isExistingSeries,
|
||||||
isSmallScreen
|
isSmallScreen
|
||||||
@@ -72,11 +78,13 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.searchResult}>
|
||||||
<Link
|
<Link
|
||||||
className={styles.searchResult}
|
className={styles.underlay}
|
||||||
{...linkProps}
|
{...linkProps}
|
||||||
>
|
/>
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
{
|
{
|
||||||
isSmallScreen ?
|
isSmallScreen ?
|
||||||
null :
|
null :
|
||||||
@@ -88,7 +96,7 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div>
|
<div className={styles.content}>
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
{title}
|
{title}
|
||||||
|
|
||||||
@@ -110,6 +118,18 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.tvdbLink}
|
||||||
|
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||||
|
onPress={this.onTVDBLinkPress}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
className={styles.tvdbLinkIcon}
|
||||||
|
name={icons.EXTERNAL_LINK}
|
||||||
|
size={28}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -146,13 +166,24 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
</Label> :
|
</Label> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
status === 'upcoming' ?
|
||||||
|
<Label
|
||||||
|
kind={kinds.INFO}
|
||||||
|
size={sizes.LARGE}
|
||||||
|
>
|
||||||
|
Upcoming
|
||||||
|
</Label> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.overview}>
|
<div className={styles.overview}>
|
||||||
{overview}
|
{overview}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
|
|
||||||
<AddNewSeriesModal
|
<AddNewSeriesModal
|
||||||
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
|
isOpen={isNewAddSeriesModalOpen && !isExistingSeries}
|
||||||
@@ -160,8 +191,10 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
title={title}
|
title={title}
|
||||||
year={year}
|
year={year}
|
||||||
overview={overview}
|
overview={overview}
|
||||||
|
folder={folder}
|
||||||
|
initialSeriesType={seriesType}
|
||||||
images={images}
|
images={images}
|
||||||
onModalClose={this.onAddSerisModalClose}
|
onModalClose={this.onAddSeriesModalClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -178,6 +211,8 @@ AddNewSeriesSearchResult.propTypes = {
|
|||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
|
folder: PropTypes.string.isRequired,
|
||||||
|
seriesType: PropTypes.string.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isExistingSeries: PropTypes.bool.isRequired,
|
isExistingSeries: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired
|
isSmallScreen: PropTypes.bool.isRequired
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
|||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
|
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
|
||||||
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
|
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
|
||||||
|
|
||||||
@@ -21,17 +21,15 @@ class ImportSeries extends Component {
|
|||||||
allSelected: false,
|
allSelected: false,
|
||||||
allUnselected: false,
|
allUnselected: false,
|
||||||
lastToggled: null,
|
lastToggled: null,
|
||||||
selectedState: {},
|
selectedState: {}
|
||||||
contentBody: null,
|
|
||||||
scrollTop: 0
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
setContentBodyRef = (ref) => {
|
setScrollerRef = (ref) => {
|
||||||
this.setState({ contentBody: ref });
|
this.setState({ scroller: ref });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -94,13 +92,13 @@ class ImportSeries extends Component {
|
|||||||
allSelected,
|
allSelected,
|
||||||
allUnselected,
|
allUnselected,
|
||||||
selectedState,
|
selectedState,
|
||||||
contentBody
|
scroller
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title="Import Series">
|
<PageContent title="Import Series">
|
||||||
<PageContentBodyConnector
|
<PageContentBody
|
||||||
ref={this.setContentBodyRef}
|
registerScroller={this.setScrollerRef}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
@@ -121,23 +119,21 @@ class ImportSeries extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
|
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
|
||||||
<ImportSeriesTableConnector
|
<ImportSeriesTableConnector
|
||||||
rootFolderId={rootFolderId}
|
rootFolderId={rootFolderId}
|
||||||
unmappedFolders={unmappedFolders}
|
unmappedFolders={unmappedFolders}
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
selectedState={selectedState}
|
selectedState={selectedState}
|
||||||
contentBody={contentBody}
|
scroller={scroller}
|
||||||
showLanguageProfile={showLanguageProfile}
|
showLanguageProfile={showLanguageProfile}
|
||||||
scrollTop={this.state.scrollTop}
|
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||||
onScroll={this.onScroll}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</PageContentBodyConnector>
|
</PageContentBody>
|
||||||
|
|
||||||
{
|
{
|
||||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
|
||||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
|
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
|
||||||
@@ -10,7 +9,6 @@ import styles from './ImportSeriesRow.css';
|
|||||||
|
|
||||||
function ImportSeriesRow(props) {
|
function ImportSeriesRow(props) {
|
||||||
const {
|
const {
|
||||||
style,
|
|
||||||
id,
|
id,
|
||||||
monitor,
|
monitor,
|
||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
@@ -26,7 +24,7 @@ function ImportSeriesRow(props) {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTableRow style={style}>
|
<>
|
||||||
<VirtualTableSelectCell
|
<VirtualTableSelectCell
|
||||||
inputClassName={styles.selectInput}
|
inputClassName={styles.selectInput}
|
||||||
id={id}
|
id={id}
|
||||||
@@ -90,14 +88,14 @@ function ImportSeriesRow(props) {
|
|||||||
<ImportSeriesSelectSeriesConnector
|
<ImportSeriesSelectSeriesConnector
|
||||||
id={id}
|
id={id}
|
||||||
isExistingSeries={isExistingSeries}
|
isExistingSeries={isExistingSeries}
|
||||||
|
onInputChange={onInputChange}
|
||||||
/>
|
/>
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
</VirtualTableRow>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ImportSeriesRow.propTypes = {
|
ImportSeriesRow.propTypes = {
|
||||||
style: PropTypes.object.isRequired,
|
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
monitor: PropTypes.string.isRequired,
|
monitor: PropTypes.string.isRequired,
|
||||||
qualityProfileId: PropTypes.number.isRequired,
|
qualityProfileId: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import VirtualTable from 'Components/Table/VirtualTable';
|
import VirtualTable from 'Components/Table/VirtualTable';
|
||||||
|
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||||
import ImportSeriesHeader from './ImportSeriesHeader';
|
import ImportSeriesHeader from './ImportSeriesHeader';
|
||||||
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
|
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
|
||||||
|
|
||||||
@@ -112,15 +113,19 @@ class ImportSeriesTable extends Component {
|
|||||||
const item = items[rowIndex];
|
const item = items[rowIndex];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ImportSeriesRowConnector
|
<VirtualTableRow
|
||||||
key={key}
|
key={key}
|
||||||
style={style}
|
style={style}
|
||||||
rootFolderId={rootFolderId}
|
>
|
||||||
showLanguageProfile={showLanguageProfile}
|
<ImportSeriesRowConnector
|
||||||
isSelected={selectedState[item.id]}
|
key={item.id}
|
||||||
onSelectedChange={onSelectedChange}
|
rootFolderId={rootFolderId}
|
||||||
id={item.id}
|
showLanguageProfile={showLanguageProfile}
|
||||||
/>
|
isSelected={selectedState[item.id]}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
id={item.id}
|
||||||
|
/>
|
||||||
|
</VirtualTableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,12 +138,10 @@ class ImportSeriesTable extends Component {
|
|||||||
allSelected,
|
allSelected,
|
||||||
allUnselected,
|
allUnselected,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
contentBody,
|
scroller,
|
||||||
showLanguageProfile,
|
showLanguageProfile,
|
||||||
scrollTop,
|
|
||||||
selectedState,
|
selectedState,
|
||||||
onSelectAllChange,
|
onSelectAllChange
|
||||||
onScroll
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
@@ -148,10 +151,9 @@ class ImportSeriesTable extends Component {
|
|||||||
return (
|
return (
|
||||||
<VirtualTable
|
<VirtualTable
|
||||||
items={items}
|
items={items}
|
||||||
contentBody={contentBody}
|
scroller={scroller}
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
rowHeight={52}
|
rowHeight={52}
|
||||||
scrollTop={scrollTop}
|
|
||||||
overscanRowCount={2}
|
overscanRowCount={2}
|
||||||
rowRenderer={this.rowRenderer}
|
rowRenderer={this.rowRenderer}
|
||||||
header={
|
header={
|
||||||
@@ -163,7 +165,6 @@ class ImportSeriesTable extends Component {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
selectedState={selectedState}
|
selectedState={selectedState}
|
||||||
onScroll={onScroll}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -183,15 +184,13 @@ ImportSeriesTable.propTypes = {
|
|||||||
selectedState: PropTypes.object.isRequired,
|
selectedState: PropTypes.object.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
allSeries: PropTypes.arrayOf(PropTypes.object),
|
allSeries: PropTypes.arrayOf(PropTypes.object),
|
||||||
contentBody: PropTypes.object.isRequired,
|
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||||
showLanguageProfile: PropTypes.bool.isRequired,
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
scrollTop: PropTypes.number.isRequired,
|
|
||||||
onSelectAllChange: PropTypes.func.isRequired,
|
onSelectAllChange: PropTypes.func.isRequired,
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
||||||
onSeriesLookup: PropTypes.func.isRequired,
|
onSeriesLookup: PropTypes.func.isRequired,
|
||||||
onSetImportSeriesValue: PropTypes.func.isRequired,
|
onSetImportSeriesValue: PropTypes.func.isRequired
|
||||||
onScroll: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImportSeriesTable;
|
export default ImportSeriesTable;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||||
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
|
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
|
||||||
|
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||||
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
|
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
@@ -41,13 +41,23 @@ class ImportSeriesSelectSeriesConnector extends Component {
|
|||||||
onSeriesSelect = (tvdbId) => {
|
onSeriesSelect = (tvdbId) => {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
items
|
items,
|
||||||
|
onInputChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const selectedSeries = items.find((item) => item.tvdbId === tvdbId);
|
||||||
|
|
||||||
this.props.setImportSeriesValue({
|
this.props.setImportSeriesValue({
|
||||||
id,
|
id,
|
||||||
selectedSeries: _.find(items, { tvdbId })
|
selectedSeries
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (selectedSeries.seriesType !== seriesTypes.STANDARD) {
|
||||||
|
onInputChange({
|
||||||
|
name: 'seriesType',
|
||||||
|
value: selectedSeries.seriesType
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -69,6 +79,7 @@ ImportSeriesSelectSeriesConnector.propTypes = {
|
|||||||
items: PropTypes.arrayOf(PropTypes.object),
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
selectedSeries: PropTypes.object,
|
selectedSeries: PropTypes.object,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
|
onInputChange: PropTypes.func.isRequired,
|
||||||
queueLookupSeries: PropTypes.func.isRequired,
|
queueLookupSeries: PropTypes.func.isRequired,
|
||||||
setImportSeriesValue: PropTypes.func.isRequired
|
setImportSeriesValue: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import FieldSet from 'Components/FieldSet';
|
|||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import RootFolders from 'RootFolder/RootFolders';
|
import RootFolders from 'RootFolder/RootFolders';
|
||||||
import styles from './ImportSeriesSelectFolder.css';
|
import styles from './ImportSeriesSelectFolder.css';
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class ImportSeriesSelectFolder extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title="Import Series">
|
<PageContent title="Import Series">
|
||||||
<PageContentBodyConnector>
|
<PageContentBody>
|
||||||
{
|
{
|
||||||
isFetching && !isPopulated &&
|
isFetching && !isPopulated &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
@@ -75,10 +75,10 @@ class ImportSeriesSelectFolder extends Component {
|
|||||||
Some tips to ensure the import goes smoothly:
|
Some tips to ensure the import goes smoothly:
|
||||||
<ul>
|
<ul>
|
||||||
<li className={styles.tip}>
|
<li className={styles.tip}>
|
||||||
Make sure your files include the quality in the name. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
|
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
|
||||||
</li>
|
</li>
|
||||||
<li className={styles.tip}>
|
<li className={styles.tip}>
|
||||||
Point Sonarr to the folder containing all of your tv shows not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
|
Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,7 +132,7 @@ class ImportSeriesSelectFolder extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</PageContentBodyConnector>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import BackupsConnector from 'System/Backup/BackupsConnector';
|
|||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
import Logs from 'System/Logs/Logs';
|
import Logs from 'System/Logs/Logs';
|
||||||
|
import Diagnostic from 'Diagnostic/Diagnostic';
|
||||||
|
|
||||||
function AppRoutes(props) {
|
function AppRoutes(props) {
|
||||||
const {
|
const {
|
||||||
@@ -229,6 +230,15 @@ function AppRoutes(props) {
|
|||||||
component={Logs}
|
component={Logs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Diagnostics
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/diag"
|
||||||
|
component={Diagnostic}
|
||||||
|
/>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Not Found
|
Not Found
|
||||||
*/}
|
*/}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import React, { Component } from 'react';
|
|||||||
import { align, icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import Measure from 'Components/Measure';
|
import Measure from 'Components/Measure';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import NoSeries from 'Series/NoSeries';
|
import NoSeries from 'Series/NoSeries';
|
||||||
@@ -76,8 +77,10 @@ class CalendarPage extends Component {
|
|||||||
filters,
|
filters,
|
||||||
hasSeries,
|
hasSeries,
|
||||||
missingEpisodeIds,
|
missingEpisodeIds,
|
||||||
|
isRssSyncExecuting,
|
||||||
isSearchingForMissing,
|
isSearchingForMissing,
|
||||||
useCurrentPage,
|
useCurrentPage,
|
||||||
|
onRssSyncPress,
|
||||||
onFilterSelect
|
onFilterSelect
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -99,6 +102,15 @@ class CalendarPage extends Component {
|
|||||||
onPress={this.onGetCalendarLinkPress}
|
onPress={this.onGetCalendarLinkPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="RSS Sync"
|
||||||
|
iconName={icons.RSS}
|
||||||
|
isSpinning={isRssSyncExecuting}
|
||||||
|
onPress={onRssSyncPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Search for Missing"
|
label="Search for Missing"
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
@@ -126,7 +138,7 @@ class CalendarPage extends Component {
|
|||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
<PageContentBodyConnector
|
<PageContentBody
|
||||||
className={styles.calendarPageBody}
|
className={styles.calendarPageBody}
|
||||||
innerClassName={styles.calendarInnerPageBody}
|
innerClassName={styles.calendarInnerPageBody}
|
||||||
>
|
>
|
||||||
@@ -147,7 +159,7 @@ class CalendarPage extends Component {
|
|||||||
hasSeries &&
|
hasSeries &&
|
||||||
<LegendConnector />
|
<LegendConnector />
|
||||||
}
|
}
|
||||||
</PageContentBodyConnector>
|
</PageContentBody>
|
||||||
|
|
||||||
<CalendarLinkModal
|
<CalendarLinkModal
|
||||||
isOpen={isCalendarLinkModalOpen}
|
isOpen={isCalendarLinkModalOpen}
|
||||||
@@ -168,10 +180,12 @@ CalendarPage.propTypes = {
|
|||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
hasSeries: PropTypes.bool.isRequired,
|
hasSeries: PropTypes.bool.isRequired,
|
||||||
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||||
isSearchingForMissing: PropTypes.bool.isRequired,
|
isSearchingForMissing: PropTypes.bool.isRequired,
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
onSearchMissingPress: PropTypes.func.isRequired,
|
onSearchMissingPress: PropTypes.func.isRequired,
|
||||||
onDaysCountChange: PropTypes.func.isRequired,
|
onDaysCountChange: PropTypes.func.isRequired,
|
||||||
|
onRssSyncPress: PropTypes.func.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ import { createSelector } from 'reselect';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { isCommandExecuting } from 'Utilities/Command';
|
import { isCommandExecuting } from 'Utilities/Command';
|
||||||
import isBefore from 'Utilities/Date/isBefore';
|
import isBefore from 'Utilities/Date/isBefore';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||||
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
|
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import CalendarPage from './CalendarPage';
|
import CalendarPage from './CalendarPage';
|
||||||
|
|
||||||
function createMissingEpisodeIdsSelector() {
|
function createMissingEpisodeIdsSelector() {
|
||||||
@@ -59,6 +62,7 @@ function createMapStateToProps() {
|
|||||||
createSeriesCountSelector(),
|
createSeriesCountSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
createMissingEpisodeIdsSelector(),
|
createMissingEpisodeIdsSelector(),
|
||||||
|
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||||
createIsSearchingSelector(),
|
createIsSearchingSelector(),
|
||||||
(
|
(
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
@@ -66,6 +70,7 @@ function createMapStateToProps() {
|
|||||||
seriesCount,
|
seriesCount,
|
||||||
uiSettings,
|
uiSettings,
|
||||||
missingEpisodeIds,
|
missingEpisodeIds,
|
||||||
|
isRssSyncExecuting,
|
||||||
isSearchingForMissing
|
isSearchingForMissing
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
@@ -74,6 +79,7 @@ function createMapStateToProps() {
|
|||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
hasSeries: !!seriesCount,
|
hasSeries: !!seriesCount,
|
||||||
missingEpisodeIds,
|
missingEpisodeIds,
|
||||||
|
isRssSyncExecuting,
|
||||||
isSearchingForMissing
|
isSearchingForMissing
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -82,6 +88,12 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
|
onRssSyncPress() {
|
||||||
|
dispatch(executeCommand({
|
||||||
|
name: commandNames.RSS_SYNC
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
onSearchMissingPress(episodeIds) {
|
onSearchMissingPress(episodeIds) {
|
||||||
dispatch(searchMissing({ episodeIds }));
|
dispatch(searchMissing({ episodeIds }));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
flex: 1 0 14.28%;
|
flex: 1 0 14.28%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 70px;
|
min-height: 70px;
|
||||||
border-bottom: 1px solid $borderColor;
|
border-bottom: 1px solid $calendarBorderColor;
|
||||||
border-left: 1px solid $borderColor;
|
border-left: 1px solid $calendarBorderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.isSingleDay {
|
.isSingleDay {
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
.dayOfMonth {
|
.dayOfMonth {
|
||||||
padding-right: 5px;
|
padding-right: 5px;
|
||||||
border-bottom: 1px solid $borderColor;
|
border-bottom: 1px solid $calendarBorderColor;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.days {
|
.days {
|
||||||
display: flex;
|
display: flex;
|
||||||
border-right: 1px solid $borderColor;
|
border-right: 1px solid $calendarBorderColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.day,
|
.day,
|
||||||
|
|||||||
@@ -16,10 +16,13 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episodeInfo {
|
||||||
|
color: $calendarTextDim;
|
||||||
|
}
|
||||||
|
|
||||||
.seriesTitle,
|
.seriesTitle,
|
||||||
.episodeTitle {
|
.episodeTitle {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
|
|
||||||
flex: 1 0 1px;
|
flex: 1 0 1px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
@@ -37,6 +40,10 @@
|
|||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.airTime {
|
||||||
|
color: $calendarTextDim;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Status
|
* Status
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ class CalendarEvent extends Component {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<div>
|
<div className={styles.airTime}>
|
||||||
{formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
|
{formatTime(airDateUtc, timeFormat)} - {formatTime(endTime.toISOString(), timeFormat, { includeMinuteZero: true })}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
|
|
||||||
.seriesTitle {
|
.seriesTitle {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
|
|
||||||
flex: 1 0 1px;
|
flex: 1 0 1px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
color: #3a3f51;
|
color: #3a3f51;
|
||||||
@@ -23,10 +22,12 @@
|
|||||||
|
|
||||||
.airTime {
|
.airTime {
|
||||||
flex: 1 0 1px;
|
flex: 1 0 1px;
|
||||||
|
color: $calendarTextDim;
|
||||||
}
|
}
|
||||||
|
|
||||||
.episodeInfo {
|
.episodeInfo {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
|
color: $calendarTextDim;
|
||||||
}
|
}
|
||||||
|
|
||||||
.absoluteEpisodeNumber {
|
.absoluteEpisodeNumber {
|
||||||
@@ -80,3 +81,7 @@
|
|||||||
.premiere {
|
.premiere {
|
||||||
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
composes: premiere from '~Calendar/Events/CalendarEvent.css';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.unaired {
|
||||||
|
composes: unaired from '~Calendar/Events/CalendarEvent.css';
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ function CalendarEventQueueDetails(props) {
|
|||||||
sizeleft,
|
sizeleft,
|
||||||
estimatedCompletionTime,
|
estimatedCompletionTime,
|
||||||
status,
|
status,
|
||||||
|
trackedDownloadState,
|
||||||
|
trackedDownloadStatus,
|
||||||
errorMessage
|
errorMessage
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = (100 - sizeleft / size * 100);
|
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueueDetails
|
<QueueDetails
|
||||||
@@ -23,6 +25,8 @@ function CalendarEventQueueDetails(props) {
|
|||||||
sizeleft={sizeleft}
|
sizeleft={sizeleft}
|
||||||
estimatedCompletionTime={estimatedCompletionTime}
|
estimatedCompletionTime={estimatedCompletionTime}
|
||||||
status={status}
|
status={status}
|
||||||
|
trackedDownloadState={trackedDownloadState}
|
||||||
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
progressBar={
|
progressBar={
|
||||||
<div title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}>
|
<div title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}>
|
||||||
@@ -44,6 +48,8 @@ CalendarEventQueueDetails.propTypes = {
|
|||||||
sizeleft: PropTypes.number.isRequired,
|
sizeleft: PropTypes.number.isRequired,
|
||||||
estimatedCompletionTime: PropTypes.string,
|
estimatedCompletionTime: PropTypes.string,
|
||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadState: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||||
errorMessage: PropTypes.string
|
errorMessage: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const APPLICATION_UPDATE = 'ApplicationUpdate';
|
export const APPLICATION_UPDATE = 'ApplicationUpdate';
|
||||||
export const BACKUP = 'Backup';
|
export const BACKUP = 'Backup';
|
||||||
export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload';
|
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
|
||||||
export const CLEAR_BLACKLIST = 'ClearBlacklist';
|
export const CLEAR_BLACKLIST = 'ClearBlacklist';
|
||||||
export const CLEAR_LOGS = 'ClearLog';
|
export const CLEAR_LOGS = 'ClearLog';
|
||||||
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class ErrorBoundary extends Component {
|
|||||||
|
|
||||||
ErrorBoundary.propTypes = {
|
ErrorBoundary.propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
errorComponent: PropTypes.func.isRequired
|
errorComponent: PropTypes.elementType.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ErrorBoundary;
|
export default ErrorBoundary;
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ class FileBrowserModalContent extends Component {
|
|||||||
<Scroller
|
<Scroller
|
||||||
ref={this.setScrollerRef}
|
ref={this.setScrollerRef}
|
||||||
className={styles.scroller}
|
className={styles.scroller}
|
||||||
|
scrollDirection={scrollDirections.BOTH}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
!!error &&
|
!!error &&
|
||||||
@@ -152,7 +153,10 @@ class FileBrowserModalContent extends Component {
|
|||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error &&
|
isPopulated && !error &&
|
||||||
<Table columns={columns}>
|
<Table
|
||||||
|
horizontalScroll={false}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{
|
{
|
||||||
emptyParent &&
|
emptyParent &&
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
filterBuilderProps,
|
filterBuilderProps,
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
|
onCancelPress,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -171,7 +172,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
filters.map((filter, index) => {
|
filters.map((filter, index) => {
|
||||||
return (
|
return (
|
||||||
<FilterBuilderRow
|
<FilterBuilderRow
|
||||||
key={index}
|
key={`${filter.key}-${index}`}
|
||||||
index={index}
|
index={index}
|
||||||
sectionItems={sectionItems}
|
sectionItems={sectionItems}
|
||||||
filterBuilderProps={filterBuilderProps}
|
filterBuilderProps={filterBuilderProps}
|
||||||
@@ -190,7 +191,7 @@ class FilterBuilderModalContent extends Component {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button onPress={onModalClose}>
|
<Button onPress={onCancelPress}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -220,6 +221,7 @@ FilterBuilderModalContent.propTypes = {
|
|||||||
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
dispatchDeleteCustomFilter: PropTypes.func.isRequired,
|
||||||
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
onSaveCustomFilterPress: PropTypes.func.isRequired,
|
||||||
dispatchSetFilter: PropTypes.func.isRequired,
|
dispatchSetFilter: PropTypes.func.isRequired,
|
||||||
|
onCancelPress: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ class FilterBuilderRowValue extends Component {
|
|||||||
tagList={tagList}
|
tagList={tagList}
|
||||||
allowNew={!tagList.length}
|
allowNew={!tagList.length}
|
||||||
kind={kinds.DEFAULT}
|
kind={kinds.DEFAULT}
|
||||||
delimiters={[9, 13]}
|
delimiters={['Tab', 'Enter']}
|
||||||
maxSuggestionsLength={100}
|
maxSuggestionsLength={100}
|
||||||
minQueryLength={0}
|
minQueryLength={0}
|
||||||
tagComponent={FilterBuilderRowValueTag}
|
tagComponent={FilterBuilderRowValueTag}
|
||||||
|
|||||||
@@ -3,16 +3,21 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
import { filterBuilderTypes } from 'Helpers/Props';
|
import { filterBuilderTypes } from 'Helpers/Props';
|
||||||
|
import * as filterTypes from 'Helpers/Props/filterTypes';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
function createTagListSelector() {
|
function createTagListSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
|
(state, { filterType }) => filterType,
|
||||||
(state, { sectionItems }) => sectionItems,
|
(state, { sectionItems }) => sectionItems,
|
||||||
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
|
(state, { selectedFilterBuilderProp }) => selectedFilterBuilderProp,
|
||||||
(sectionItems, selectedFilterBuilderProp) => {
|
(filterType, sectionItems, selectedFilterBuilderProp) => {
|
||||||
if (
|
if (
|
||||||
selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
(selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
|
||||||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING
|
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
|
||||||
|
filterType !== filterTypes.EQUAL &&
|
||||||
|
filterType !== filterBuilderTypes.NOT_EQUAL ||
|
||||||
|
!selectedFilterBuilderProp.optionsSelector
|
||||||
) {
|
) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
.tag {
|
.tag {
|
||||||
|
height: 21px;
|
||||||
|
|
||||||
&.isLastTag {
|
&.isLastTag {
|
||||||
.or {
|
.or {
|
||||||
display: none;
|
display: none;
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
const protocols = [
|
const seriesStatusList = [
|
||||||
{ id: 'continuing', name: 'Continuing' },
|
{ id: 'continuing', name: 'Continuing' },
|
||||||
|
{ id: 'upcoming', name: 'Upcoming' },
|
||||||
{ id: 'ended', name: 'Ended' }
|
{ id: 'ended', name: 'Ended' }
|
||||||
];
|
];
|
||||||
|
|
||||||
function SeriesStatusFilterBuilderRowValue(props) {
|
function SeriesStatusFilterBuilderRowValue(props) {
|
||||||
return (
|
return (
|
||||||
<FilterBuilderRowValue
|
<FilterBuilderRowValue
|
||||||
tagList={protocols}
|
tagList={seriesStatusList}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ function CustomFiltersModalContent(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
customFilters.map((customFilter, index) => {
|
customFilters.map((customFilter) => {
|
||||||
return (
|
return (
|
||||||
<CustomFilter
|
<CustomFilter
|
||||||
key={index}
|
key={customFilter.id}
|
||||||
id={customFilter.id}
|
id={customFilter.id}
|
||||||
label={customFilter.label}
|
label={customFilter.label}
|
||||||
filters={customFilter.filters}
|
filters={customFilter.filters}
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ class FilterModal extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCancelPress = () => {
|
||||||
|
if (this.state.filterBuilder) {
|
||||||
|
this.setState({
|
||||||
|
filterBuilder: false,
|
||||||
|
id: null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.onModalClose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onModalClose = () => {
|
onModalClose = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
filterBuilder: false,
|
filterBuilder: false,
|
||||||
@@ -67,6 +78,7 @@ class FilterModal extends Component {
|
|||||||
<FilterBuilderModalContentConnector
|
<FilterBuilderModalContentConnector
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
id={id}
|
id={id}
|
||||||
|
onCancelPress={this.onCancelPress}
|
||||||
onModalClose={this.onModalClose}
|
onModalClose={this.onModalClose}
|
||||||
/> :
|
/> :
|
||||||
<CustomFiltersModalContentConnector
|
<CustomFiltersModalContentConnector
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
.suggestionsList {
|
.suggestionsList {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
|
max-height: 200px;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ class AutoSuggestInput extends Component {
|
|||||||
className: classNames(
|
className: classNames(
|
||||||
className,
|
className,
|
||||||
hasError && styles.hasError,
|
hasError && styles.hasError,
|
||||||
hasWarning && styles.hasWarning,
|
hasWarning && styles.hasWarning
|
||||||
),
|
),
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
@@ -234,7 +234,7 @@ AutoSuggestInput.propTypes = {
|
|||||||
minHeight: PropTypes.number.isRequired,
|
minHeight: PropTypes.number.isRequired,
|
||||||
maxHeight: PropTypes.number.isRequired,
|
maxHeight: PropTypes.number.isRequired,
|
||||||
getSuggestionValue: PropTypes.func.isRequired,
|
getSuggestionValue: PropTypes.func.isRequired,
|
||||||
renderInputComponent: PropTypes.func,
|
renderInputComponent: PropTypes.elementType,
|
||||||
renderSuggestion: PropTypes.func.isRequired,
|
renderSuggestion: PropTypes.func.isRequired,
|
||||||
onInputChange: PropTypes.func,
|
onInputChange: PropTypes.func,
|
||||||
onInputKeyDown: PropTypes.func,
|
onInputKeyDown: PropTypes.func,
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { fetchDevices, clearDevices } from 'Store/Actions/deviceActions';
|
import { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
|
||||||
import DeviceInput from './DeviceInput';
|
import DeviceInput from './DeviceInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(state) => state.devices,
|
(state) => state.providerOptions,
|
||||||
(value, devices) => {
|
(value, devices) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -37,8 +37,8 @@ function createMapStateToProps() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
dispatchFetchDevices: fetchDevices,
|
dispatchFetchOptions: fetchOptions,
|
||||||
dispatchClearDevices: clearDevices
|
dispatchClearOptions: clearOptions
|
||||||
};
|
};
|
||||||
|
|
||||||
class DeviceInputConnector extends Component {
|
class DeviceInputConnector extends Component {
|
||||||
@@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
// this.props.dispatchClearDevices();
|
this.props.dispatchClearOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -61,10 +61,14 @@ class DeviceInputConnector extends Component {
|
|||||||
const {
|
const {
|
||||||
provider,
|
provider,
|
||||||
providerData,
|
providerData,
|
||||||
dispatchFetchDevices
|
dispatchFetchOptions
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatchFetchDevices({ provider, providerData });
|
dispatchFetchOptions({
|
||||||
|
action: 'getDevices',
|
||||||
|
provider,
|
||||||
|
providerData
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -92,8 +96,8 @@ DeviceInputConnector.propTypes = {
|
|||||||
providerData: PropTypes.object.isRequired,
|
providerData: PropTypes.object.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
dispatchFetchDevices: PropTypes.func.isRequired,
|
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||||
dispatchClearDevices: PropTypes.func.isRequired
|
dispatchClearOptions: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
export default connect(createMapStateToProps, mapDispatchToProps)(DeviceInputConnector);
|
||||||
|
|||||||
@@ -1,19 +1,8 @@
|
|||||||
.enhancedSelect {
|
.enhancedSelect {
|
||||||
composes: input from '~Components/Form/Input.css';
|
composes: input from '~Components/Form/Input.css';
|
||||||
composes: link from '~Components/Link/Link.css';
|
|
||||||
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 6px 16px;
|
|
||||||
width: 100%;
|
|
||||||
height: 35px;
|
|
||||||
border: 1px solid $inputBorderColor;
|
|
||||||
border-radius: 4px;
|
|
||||||
background-color: $white;
|
|
||||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
|
||||||
color: $black;
|
|
||||||
cursor: default;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
@@ -56,6 +45,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
|
max-height: 100%;
|
||||||
width: 350px !important;
|
width: 350px !important;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import React, { Component } from 'react';
|
|||||||
import { Manager, Popper, Reference } from 'react-popper';
|
import { Manager, Popper, Reference } from 'react-popper';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||||
import isMobileUtil from 'Utilities/isMobile';
|
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||||
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
import * as keyCodes from 'Utilities/Constants/keyCodes';
|
||||||
import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
@@ -262,6 +262,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
isDisabled,
|
isDisabled,
|
||||||
hasError,
|
hasError,
|
||||||
hasWarning,
|
hasWarning,
|
||||||
|
valueOptions,
|
||||||
selectedValueOptions,
|
selectedValueOptions,
|
||||||
selectedValueComponent: SelectedValueComponent,
|
selectedValueComponent: SelectedValueComponent,
|
||||||
optionComponent: OptionComponent
|
optionComponent: OptionComponent
|
||||||
@@ -363,6 +364,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
key={v.key}
|
key={v.key}
|
||||||
id={v.key}
|
id={v.key}
|
||||||
isSelected={index === selectedIndex}
|
isSelected={index === selectedIndex}
|
||||||
|
{...valueOptions}
|
||||||
{...v}
|
{...v}
|
||||||
isMobile={false}
|
isMobile={false}
|
||||||
onSelect={this.onSelect}
|
onSelect={this.onSelect}
|
||||||
@@ -404,6 +406,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
key={v.key}
|
key={v.key}
|
||||||
id={v.key}
|
id={v.key}
|
||||||
isSelected={index === selectedIndex}
|
isSelected={index === selectedIndex}
|
||||||
|
{...valueOptions}
|
||||||
{...v}
|
{...v}
|
||||||
isMobile={true}
|
isMobile={true}
|
||||||
onSelect={this.onSelect}
|
onSelect={this.onSelect}
|
||||||
@@ -431,9 +434,10 @@ EnhancedSelectInput.propTypes = {
|
|||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
hasWarning: PropTypes.bool,
|
hasWarning: PropTypes.bool,
|
||||||
|
valueOptions: PropTypes.object.isRequired,
|
||||||
selectedValueOptions: PropTypes.object.isRequired,
|
selectedValueOptions: PropTypes.object.isRequired,
|
||||||
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
selectedValueComponent: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
|
||||||
optionComponent: PropTypes.func,
|
optionComponent: PropTypes.elementType,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -441,6 +445,7 @@ EnhancedSelectInput.defaultProps = {
|
|||||||
className: styles.enhancedSelect,
|
className: styles.enhancedSelect,
|
||||||
disabledClassName: styles.isDisabled,
|
disabledClassName: styles.isDisabled,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
valueOptions: {},
|
||||||
selectedValueOptions: {},
|
selectedValueOptions: {},
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||||
optionComponent: HintedSelectInputOption
|
optionComponent: HintedSelectInputOption
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
|
|
||||||
EnhancedSelectInputOption.propTypes = {
|
EnhancedSelectInputOption.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
isHidden: PropTypes.bool.isRequired,
|
isHidden: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import PasswordInput from './PasswordInput';
|
|||||||
import PathInputConnector from './PathInputConnector';
|
import PathInputConnector from './PathInputConnector';
|
||||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||||
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
|
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
|
||||||
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
@@ -61,6 +62,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.LANGUAGE_PROFILE_SELECT:
|
case inputTypes.LANGUAGE_PROFILE_SELECT:
|
||||||
return LanguageProfileSelectInputConnector;
|
return LanguageProfileSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.INDEXER_SELECT:
|
||||||
|
return IndexerSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.ROOT_FOLDER_SELECT:
|
case inputTypes.ROOT_FOLDER_SELECT:
|
||||||
return RootFolderSelectInputConnector;
|
return RootFolderSelectInputConnector;
|
||||||
|
|
||||||
|
|||||||
96
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
96
frontend/src/Components/Form/IndexerSelectInputConnector.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.settings.indexers,
|
||||||
|
(state, { includeAny }) => includeAny,
|
||||||
|
(indexers, includeAny) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items
|
||||||
|
} = indexers;
|
||||||
|
|
||||||
|
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||||
|
return {
|
||||||
|
key: indexer.id,
|
||||||
|
value: indexer.name
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
if (includeAny) {
|
||||||
|
values.unshift({
|
||||||
|
key: 0,
|
||||||
|
value: '(Any)'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
values
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchIndexers: fetchIndexers
|
||||||
|
};
|
||||||
|
|
||||||
|
class IndexerSelectInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (!this.props.isPopulated) {
|
||||||
|
this.props.dispatchFetchIndexers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onChange = ({ name, value }) => {
|
||||||
|
this.props.onChange({ name, value: parseInt(value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...this.props}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IndexerSelectInputConnector.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
includeAny: PropTypes.bool.isRequired,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
IndexerSelectInputConnector.defaultProps = {
|
||||||
|
includeAny: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
||||||
@@ -98,7 +98,9 @@ class KeyValueListInput extends Component {
|
|||||||
className,
|
className,
|
||||||
value,
|
value,
|
||||||
keyPlaceholder,
|
keyPlaceholder,
|
||||||
valuePlaceholder
|
valuePlaceholder,
|
||||||
|
hasError,
|
||||||
|
hasWarning
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const { isFocused } = this.state;
|
const { isFocused } = this.state;
|
||||||
@@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className={classNames(
|
<div className={classNames(
|
||||||
className,
|
className,
|
||||||
isFocused && styles.isFocused
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inputWrapper {
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonWrapper {
|
||||||
|
flex: 0 0 22px;
|
||||||
|
}
|
||||||
|
|
||||||
.keyInput,
|
.keyInput,
|
||||||
.valueInput {
|
.valueInput {
|
||||||
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,34 +63,41 @@ class KeyValueListInputItem extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.itemContainer}>
|
<div className={styles.itemContainer}>
|
||||||
<TextInput
|
<div className={styles.inputWrapper}>
|
||||||
className={styles.keyInput}
|
<TextInput
|
||||||
name="key"
|
className={styles.keyInput}
|
||||||
value={keyValue}
|
name="key"
|
||||||
placeholder={keyPlaceholder}
|
value={keyValue}
|
||||||
onChange={this.onKeyChange}
|
placeholder={keyPlaceholder}
|
||||||
onFocus={this.onFocus}
|
onChange={this.onKeyChange}
|
||||||
onBlur={this.onBlur}
|
onFocus={this.onFocus}
|
||||||
/>
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TextInput
|
<div className={styles.inputWrapper}>
|
||||||
className={styles.valueInput}
|
<TextInput
|
||||||
name="value"
|
className={styles.valueInput}
|
||||||
value={value}
|
name="value"
|
||||||
placeholder={valuePlaceholder}
|
value={value}
|
||||||
onChange={this.onValueChange}
|
placeholder={valuePlaceholder}
|
||||||
onFocus={this.onFocus}
|
onChange={this.onValueChange}
|
||||||
onBlur={this.onBlur}
|
onFocus={this.onFocus}
|
||||||
/>
|
onBlur={this.onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
<div className={styles.buttonWrapper}>
|
||||||
!isNew &&
|
{
|
||||||
<IconButton
|
isNew ?
|
||||||
name={icons.REMOVE}
|
null :
|
||||||
tabIndex={-1}
|
<IconButton
|
||||||
onPress={this.onRemovePress}
|
name={icons.REMOVE}
|
||||||
/>
|
tabIndex={-1}
|
||||||
}
|
onPress={this.onRemovePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.languageProfiles,
|
createSortedSectionSelector('settings.languageProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(languageProfiles, includeNoChange, includeMixed) => {
|
(languageProfiles, includeNoChange, includeMixed) => {
|
||||||
const values = _.map(languageProfiles.items.sort(sortByName), (languageProfile) => {
|
const values = _.map(languageProfiles.items, (languageProfile) => {
|
||||||
return {
|
return {
|
||||||
key: languageProfile.id,
|
key: languageProfile.id,
|
||||||
value: languageProfile.name
|
value: languageProfile.name
|
||||||
|
|||||||
@@ -18,10 +18,19 @@ class PathInput extends Component {
|
|||||||
this._node = document.getElementById('portal-root');
|
this._node = document.getElementById('portal-root');
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
value: props.value,
|
||||||
isFileBrowserModalOpen: false
|
isFileBrowserModalOpen: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const { value } = this.props;
|
||||||
|
|
||||||
|
if (prevProps.value !== value) {
|
||||||
|
this.setState({ value });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
@@ -51,11 +60,8 @@ class PathInput extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onInputChange = (event, { newValue }) => {
|
onInputChange = ({ value }) => {
|
||||||
this.props.onChange({
|
this.setState({ value });
|
||||||
name: this.props.name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onInputKeyDown = (event) => {
|
onInputKeyDown = (event) => {
|
||||||
@@ -77,6 +83,11 @@ class PathInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onInputBlur = () => {
|
onInputBlur = () => {
|
||||||
|
this.props.onChange({
|
||||||
|
name: this.props.name,
|
||||||
|
value: this.state.value
|
||||||
|
});
|
||||||
|
|
||||||
this.props.onClearPaths();
|
this.props.onClearPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,13 +119,18 @@ class PathInput extends Component {
|
|||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
name,
|
name,
|
||||||
value,
|
|
||||||
paths,
|
paths,
|
||||||
includeFiles,
|
includeFiles,
|
||||||
hasFileBrowser,
|
hasFileBrowser,
|
||||||
onChange,
|
onChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
isFileBrowserModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<AutoSuggestInput
|
<AutoSuggestInput
|
||||||
@@ -130,7 +146,7 @@ class PathInput extends Component {
|
|||||||
onSuggestionSelected={this.onSuggestionSelected}
|
onSuggestionSelected={this.onSuggestionSelected}
|
||||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||||
onChange={onChange}
|
onChange={this.onInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -144,7 +160,7 @@ class PathInput extends Component {
|
|||||||
</FormInputButton>
|
</FormInputButton>
|
||||||
|
|
||||||
<FileBrowserModal
|
<FileBrowserModal
|
||||||
isOpen={this.state.isFileBrowserModalOpen}
|
isOpen={isFileBrowserModalOpen}
|
||||||
name={name}
|
name={name}
|
||||||
value={value}
|
value={value}
|
||||||
includeFiles={includeFiles}
|
includeFiles={includeFiles}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function getType(type) {
|
|||||||
return inputTypes.NUMBER;
|
return inputTypes.NUMBER;
|
||||||
case 'path':
|
case 'path':
|
||||||
return inputTypes.PATH;
|
return inputTypes.PATH;
|
||||||
case 'filepath':
|
case 'filePath':
|
||||||
return inputTypes.PATH;
|
return inputTypes.PATH;
|
||||||
case 'select':
|
case 'select':
|
||||||
return inputTypes.SELECT;
|
return inputTypes.SELECT;
|
||||||
@@ -28,7 +28,7 @@ function getType(type) {
|
|||||||
return inputTypes.TEXT_TAG;
|
return inputTypes.TEXT_TAG;
|
||||||
case 'textbox':
|
case 'textbox':
|
||||||
return inputTypes.TEXT;
|
return inputTypes.TEXT;
|
||||||
case 'oauth':
|
case 'oAuth':
|
||||||
return inputTypes.OAUTH;
|
return inputTypes.OAUTH;
|
||||||
default:
|
default:
|
||||||
return inputTypes.TEXT;
|
return inputTypes.TEXT;
|
||||||
@@ -60,6 +60,7 @@ function ProviderFieldFormGroup(props) {
|
|||||||
value,
|
value,
|
||||||
type,
|
type,
|
||||||
advanced,
|
advanced,
|
||||||
|
hidden,
|
||||||
pending,
|
pending,
|
||||||
errors,
|
errors,
|
||||||
warnings,
|
warnings,
|
||||||
@@ -68,6 +69,13 @@ function ProviderFieldFormGroup(props) {
|
|||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
if (
|
||||||
|
hidden === 'hidden' ||
|
||||||
|
(hidden === 'hiddenIfNotSet' && !value)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
@@ -86,7 +94,7 @@ function ProviderFieldFormGroup(props) {
|
|||||||
errors={errors}
|
errors={errors}
|
||||||
warnings={warnings}
|
warnings={warnings}
|
||||||
pending={pending}
|
pending={pending}
|
||||||
includeFiles={type === 'filepath' ? true : undefined}
|
includeFiles={type === 'filePath' ? true : undefined}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
@@ -108,6 +116,7 @@ ProviderFieldFormGroup.propTypes = {
|
|||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
advanced: PropTypes.bool.isRequired,
|
advanced: PropTypes.bool.isRequired,
|
||||||
|
hidden: PropTypes.string,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import sortByName from 'Utilities/Array/sortByName';
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.qualityProfiles,
|
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(state, { includeMixed }) => includeMixed,
|
(state, { includeMixed }) => includeMixed,
|
||||||
(qualityProfiles, includeNoChange, includeMixed) => {
|
(qualityProfiles, includeNoChange, includeMixed) => {
|
||||||
const values = _.map(qualityProfiles.items.sort(sortByName), (qualityProfile) => {
|
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
||||||
return {
|
return {
|
||||||
key: qualityProfile.id,
|
key: qualityProfile.id,
|
||||||
value: qualityProfile.name
|
value: qualityProfile.name
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
@@ -13,7 +12,7 @@ function createMapStateToProps() {
|
|||||||
(state) => state.rootFolders,
|
(state) => state.rootFolders,
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
(rootFolders, includeNoChange) => {
|
(rootFolders, includeNoChange) => {
|
||||||
const values = _.map(rootFolders.items, (rootFolder) => {
|
const values = rootFolders.items.map((rootFolder) => {
|
||||||
return {
|
return {
|
||||||
key: rootFolder.path,
|
key: rootFolder.path,
|
||||||
value: rootFolder.path,
|
value: rootFolder.path,
|
||||||
@@ -85,7 +84,7 @@ class RootFolderSelectInputConnector extends Component {
|
|||||||
onChange
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!value || !_.some(values, (v) => v.key === value) || value === ADD_NEW_KEY) {
|
if (!value || !values.some((v) => v.key === value) || value === ADD_NEW_KEY) {
|
||||||
const defaultValue = values[0];
|
const defaultValue = values[0];
|
||||||
|
|
||||||
if (defaultValue.key === ADD_NEW_KEY) {
|
if (defaultValue.key === ADD_NEW_KEY) {
|
||||||
@@ -96,6 +95,27 @@ class RootFolderSelectInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.values === values) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!value && values.length && values.some((v) => !!v.key && v.key !== ADD_NEW_KEY)) {
|
||||||
|
const defaultValue = values[0];
|
||||||
|
|
||||||
|
if (defaultValue.key !== ADD_NEW_KEY) {
|
||||||
|
onChange({ name, value: defaultValue.key });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seriesFolder {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: $disabledColor;
|
||||||
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
color: $darkGray;
|
color: $darkGray;
|
||||||
|
|||||||
@@ -7,14 +7,20 @@ import styles from './RootFolderSelectInputOption.css';
|
|||||||
|
|
||||||
function RootFolderSelectInputOption(props) {
|
function RootFolderSelectInputOption(props) {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
value,
|
value,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
|
seriesFolder,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
isWindows,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const slashCharacter = isWindows ? '\\' : '/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputOption
|
<EnhancedSelectInputOption
|
||||||
|
id={id}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
@@ -23,7 +29,18 @@ function RootFolderSelectInputOption(props) {
|
|||||||
isMobile && styles.isMobile
|
isMobile && styles.isMobile
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>{value}</div>
|
<div className={styles.value}>
|
||||||
|
{value}
|
||||||
|
|
||||||
|
{
|
||||||
|
seriesFolder && id !== 'addNew' ?
|
||||||
|
<div className={styles.seriesFolder}>
|
||||||
|
{slashCharacter}
|
||||||
|
{seriesFolder}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
freeSpace != null &&
|
freeSpace != null &&
|
||||||
@@ -37,9 +54,12 @@ function RootFolderSelectInputOption(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RootFolderSelectInputOption.propTypes = {
|
RootFolderSelectInputOption.propTypes = {
|
||||||
|
id: PropTypes.string.isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
isMobile: PropTypes.bool.isRequired
|
seriesFolder: PropTypes.string,
|
||||||
|
isMobile: PropTypes.bool.isRequired,
|
||||||
|
isWindows: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RootFolderSelectInputOption;
|
export default RootFolderSelectInputOption;
|
||||||
|
|||||||
@@ -7,10 +7,20 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pathContainer {
|
||||||
|
@add-mixin truncate;
|
||||||
|
display: flex;
|
||||||
|
flex: 1 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.path {
|
.path {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
flex: 1 0 0;
|
.seriesFolder {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
color: $disabledColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
|
|||||||
@@ -8,17 +8,32 @@ function RootFolderSelectInputSelectedValue(props) {
|
|||||||
const {
|
const {
|
||||||
value,
|
value,
|
||||||
freeSpace,
|
freeSpace,
|
||||||
|
seriesFolder,
|
||||||
includeFreeSpace,
|
includeFreeSpace,
|
||||||
|
isWindows,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const slashCharacter = isWindows ? '\\' : '/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputSelectedValue
|
<EnhancedSelectInputSelectedValue
|
||||||
className={styles.selectedValue}
|
className={styles.selectedValue}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<div className={styles.path}>
|
<div className={styles.pathContainer}>
|
||||||
{value}
|
<div className={styles.path}>
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
seriesFolder ?
|
||||||
|
<div className={styles.seriesFolder}>
|
||||||
|
{slashCharacter}
|
||||||
|
{seriesFolder}
|
||||||
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -34,6 +49,8 @@ function RootFolderSelectInputSelectedValue(props) {
|
|||||||
RootFolderSelectInputSelectedValue.propTypes = {
|
RootFolderSelectInputSelectedValue.propTypes = {
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
freeSpace: PropTypes.number,
|
freeSpace: PropTypes.number,
|
||||||
|
seriesFolder: PropTypes.string,
|
||||||
|
isWindows: PropTypes.bool,
|
||||||
includeFreeSpace: PropTypes.bool.isRequired
|
includeFreeSpace: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||||
import SelectInput from './SelectInput';
|
import SelectInput from './SelectInput';
|
||||||
|
|
||||||
const seriesTypeOptions = [
|
const seriesTypeOptions = [
|
||||||
{ key: 'standard', value: 'Standard' },
|
{ key: seriesTypes.STANDARD, value: 'Standard' },
|
||||||
{ key: 'daily', value: 'Daily' },
|
{ key: seriesTypes.DAILY, value: 'Daily' },
|
||||||
{ key: 'anime', value: 'Anime' }
|
{ key: seriesTypes.ANIME, value: 'Anime' }
|
||||||
];
|
];
|
||||||
|
|
||||||
function SeriesTypeSelectInput(props) {
|
function SeriesTypeSelectInput(props) {
|
||||||
|
|||||||
@@ -12,12 +12,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hasError {
|
||||||
|
composes: hasError from '~Components/Form/Input.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasWarning {
|
||||||
|
composes: hasWarning from '~Components/Form/Input.css';
|
||||||
|
}
|
||||||
|
|
||||||
.internalInput {
|
.internalInput {
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
min-width: 20%;
|
min-width: 20%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
width: 0%;
|
width: 0%;
|
||||||
height: 21px;
|
height: 31px;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ class TagInput extends Component {
|
|||||||
suggestions
|
suggestions
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const keyCode = event.keyCode;
|
const key = event.key;
|
||||||
|
|
||||||
if (keyCode === 8 && !value.length) {
|
if (key === 'Backspace' && !value.length) {
|
||||||
const index = tags.length - 1;
|
const index = tags.length - 1;
|
||||||
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -116,7 +116,7 @@ class TagInput extends Component {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delimiters.includes(keyCode)) {
|
if (delimiters.includes(key)) {
|
||||||
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
|
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
|
||||||
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
||||||
|
|
||||||
@@ -210,6 +210,8 @@ class TagInput extends Component {
|
|||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
inputContainerClassName,
|
inputContainerClassName,
|
||||||
|
hasError,
|
||||||
|
hasWarning,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -227,6 +229,8 @@ class TagInput extends Component {
|
|||||||
inputContainerClassName={classNames(
|
inputContainerClassName={classNames(
|
||||||
inputContainerClassName,
|
inputContainerClassName,
|
||||||
isFocused && styles.isFocused,
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
)}
|
)}
|
||||||
value={value}
|
value={value}
|
||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
@@ -256,11 +260,11 @@ TagInput.propTypes = {
|
|||||||
allowNew: PropTypes.bool.isRequired,
|
allowNew: PropTypes.bool.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
placeholder: PropTypes.string.isRequired,
|
placeholder: PropTypes.string.isRequired,
|
||||||
delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
|
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
minQueryLength: PropTypes.number.isRequired,
|
minQueryLength: PropTypes.number.isRequired,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
hasWarning: PropTypes.bool,
|
hasWarning: PropTypes.bool,
|
||||||
tagComponent: PropTypes.func.isRequired,
|
tagComponent: PropTypes.elementType.isRequired,
|
||||||
onTagAdd: PropTypes.func.isRequired,
|
onTagAdd: PropTypes.func.isRequired,
|
||||||
onTagDelete: PropTypes.func.isRequired
|
onTagDelete: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
@@ -271,8 +275,7 @@ TagInput.defaultProps = {
|
|||||||
allowNew: true,
|
allowNew: true,
|
||||||
kind: kinds.INFO,
|
kind: kinds.INFO,
|
||||||
placeholder: '',
|
placeholder: '',
|
||||||
// Tab, enter, space and comma
|
delimiters: ['Tab', 'Enter', ' ', ','],
|
||||||
delimiters: [9, 13, 32, 188],
|
|
||||||
minQueryLength: 1,
|
minQueryLength: 1,
|
||||||
tagComponent: TagInputTag
|
tagComponent: TagInputTag
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
left: -1px;
|
left: -1px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: start;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
padding: 6px 16px;
|
padding: 1px 16px;
|
||||||
|
min-height: 33px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ TagInputInput.propTypes = {
|
|||||||
inputProps: PropTypes.object.isRequired,
|
inputProps: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||||
isFocused: PropTypes.bool.isRequired,
|
isFocused: PropTypes.bool.isRequired,
|
||||||
tagComponent: PropTypes.func.isRequired,
|
tagComponent: PropTypes.elementType.isRequired,
|
||||||
onTagDelete: PropTypes.func.isRequired,
|
onTagDelete: PropTypes.func.isRequired,
|
||||||
onInputContainerPress: PropTypes.func.isRequired
|
onInputContainerPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
5
frontend/src/Components/Form/TagInputTag.css
Normal file
5
frontend/src/Components/Form/TagInputTag.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.tag {
|
||||||
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
|
height: 31px;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import { kinds } from 'Helpers/Props';
|
|||||||
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
import tagShape from 'Helpers/Props/Shapes/tagShape';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import styles from './TagInputTag.css';
|
||||||
|
|
||||||
class TagInputTag extends Component {
|
class TagInputTag extends Component {
|
||||||
|
|
||||||
@@ -31,9 +32,9 @@ class TagInputTag extends Component {
|
|||||||
tag,
|
tag,
|
||||||
kind
|
kind
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
|
className={styles.tag}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
onPress={this.onDelete}
|
onPress={this.onDelete}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -25,3 +25,7 @@
|
|||||||
.warning {
|
.warning {
|
||||||
color: $warningColor;
|
color: $warningColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.purple {
|
||||||
|
color: $purple;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import styles from './LoadingIndicator.css';
|
import styles from './LoadingIndicator.css';
|
||||||
|
|
||||||
function LoadingIndicator({ className, size }) {
|
function LoadingIndicator({ className, rippleClassName, size }) {
|
||||||
const sizeInPx = `${size}px`;
|
const sizeInPx = `${size}px`;
|
||||||
const width = sizeInPx;
|
const width = sizeInPx;
|
||||||
const height = sizeInPx;
|
const height = sizeInPx;
|
||||||
@@ -17,17 +17,17 @@ function LoadingIndicator({ className, size }) {
|
|||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={styles.ripple}
|
className={rippleClassName}
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles.ripple}
|
className={rippleClassName}
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={styles.ripple}
|
className={rippleClassName}
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,11 +37,13 @@ function LoadingIndicator({ className, size }) {
|
|||||||
|
|
||||||
LoadingIndicator.propTypes = {
|
LoadingIndicator.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
rippleClassName: PropTypes.string,
|
||||||
size: PropTypes.number
|
size: PropTypes.number
|
||||||
};
|
};
|
||||||
|
|
||||||
LoadingIndicator.defaultProps = {
|
LoadingIndicator.defaultProps = {
|
||||||
className: styles.loading,
|
className: styles.loading,
|
||||||
|
rippleClassName: styles.ripple,
|
||||||
size: 50
|
size: 50
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user