Compare commits
381 Commits
phantom-di
...
v3.0.5.114
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90697d77a5 | ||
|
|
fa7aa05d60 | ||
|
|
4ed5fefcc6 | ||
|
|
4c324fbbbf | ||
|
|
7da02c236a | ||
|
|
79cfa3a5f6 | ||
|
|
2746556ae2 | ||
|
|
d668e923af | ||
|
|
24ca47356e | ||
|
|
ab4f57f2fa | ||
|
|
13ff2d4c70 | ||
|
|
2728bf79ca | ||
|
|
cd28af98ee | ||
|
|
e9818b9982 | ||
|
|
d6cf370bcd | ||
|
|
cb8ed74fe9 | ||
|
|
4e81b33006 | ||
|
|
e67864fecb | ||
|
|
e289c428c6 | ||
|
|
23047623ee | ||
|
|
062e47e27e | ||
|
|
28ba037630 | ||
|
|
82da38941e | ||
|
|
10c770b116 | ||
|
|
3c45349404 | ||
|
|
b815d27a10 | ||
|
|
ec698c2cf7 | ||
|
|
e7d57a95f2 | ||
|
|
1250d71e80 | ||
|
|
e42d1af5ff | ||
|
|
88ad6f9544 | ||
|
|
54c386dd22 | ||
|
|
694360457d | ||
|
|
ae196af2ad | ||
|
|
12fafb2457 | ||
|
|
795bc91d25 | ||
|
|
044342f677 | ||
|
|
5960035d5d | ||
|
|
6c324c8a1c | ||
|
|
54a267d860 | ||
|
|
b5f08a8f06 | ||
|
|
653db8290e | ||
|
|
cbc4295f28 | ||
|
|
8876c9194d | ||
|
|
d898f55660 | ||
|
|
a85979c2f6 | ||
|
|
ae63373b2b | ||
|
|
044cb563a6 | ||
|
|
5302ee05bc | ||
|
|
29bc660cfb | ||
|
|
f8b8afdaa2 | ||
|
|
33b708927c | ||
|
|
42d9e37e7d | ||
|
|
fc3bea370f | ||
|
|
a1ddcf2b7b | ||
|
|
5b98a17873 | ||
|
|
8fd4adbdb6 | ||
|
|
952a7248c9 | ||
|
|
577604fccc | ||
|
|
3d3cd8cf5c | ||
|
|
5e4c9dfe60 | ||
|
|
f3f2648ce5 | ||
|
|
d1c3ae1749 | ||
|
|
1cbcad6960 | ||
|
|
ab45910e56 | ||
|
|
53f5694535 | ||
|
|
7a3f4e8033 | ||
|
|
474f4bcc6d | ||
|
|
ec058436d3 | ||
|
|
39ca348666 | ||
|
|
02a46349a2 | ||
|
|
98dc20d919 | ||
|
|
a510c9e86d | ||
|
|
f5d690aa7b | ||
|
|
c91fabcf2d | ||
|
|
21fafb895f | ||
|
|
8047e5aa67 | ||
|
|
546d65b663 | ||
|
|
328cfa12f6 | ||
|
|
d475fccb55 | ||
|
|
50a2e52c19 | ||
|
|
28e0ad38b0 | ||
|
|
b66bf542c1 | ||
|
|
c05fccb90d | ||
|
|
ab478fd64b | ||
|
|
d016079f6b | ||
|
|
4a7e5ac06e | ||
|
|
e10cff5414 | ||
|
|
6c17b4bb86 | ||
|
|
e704ee617f | ||
|
|
c2fcdb4457 | ||
|
|
a6637b2911 | ||
|
|
efb18223fe | ||
|
|
c3e2f22adb | ||
|
|
9de5181f01 | ||
|
|
63607ad541 | ||
|
|
620359dcc6 | ||
|
|
881bbb654b | ||
|
|
c28cafba0a | ||
|
|
5668152d6f | ||
|
|
dcda03da4a | ||
|
|
ba2ca7ee29 | ||
|
|
8077434a38 | ||
|
|
a225b34806 | ||
|
|
b181f8ae9f | ||
|
|
579c443349 | ||
|
|
8a511a0e20 | ||
|
|
8bc0ab981c | ||
|
|
741fa57f05 | ||
|
|
8a3027fa7c | ||
|
|
fddf2d1fc8 | ||
|
|
6d44d6c1b7 | ||
|
|
7e045f3e3c | ||
|
|
f4d14301f1 | ||
|
|
17985f7a33 | ||
|
|
772448b41b | ||
|
|
ed2bb0d73a | ||
|
|
ae45089c62 | ||
|
|
f3695a41d7 | ||
|
|
471f0016b4 | ||
|
|
056a699daf | ||
|
|
99be6a7e40 | ||
|
|
c1d060ff58 | ||
|
|
101b1ec743 | ||
|
|
a4ffb256a6 | ||
|
|
ca52eb76ca | ||
|
|
37501094d7 | ||
|
|
cfbb4a3235 | ||
|
|
a2050803a2 | ||
|
|
e5e86680c8 | ||
|
|
ca34f64eb0 | ||
|
|
c7b950f213 | ||
|
|
36f231ea24 | ||
|
|
b60d4f46d2 | ||
|
|
090cdc364e | ||
|
|
182ad17b77 | ||
|
|
026af22229 | ||
|
|
078898af91 | ||
|
|
8b8deb5646 | ||
|
|
314a12ffb5 | ||
|
|
7b04e11c54 | ||
|
|
9180e7d6fd | ||
|
|
b8b09cd0ce | ||
|
|
39cb0934bc | ||
|
|
b84f84ad0a | ||
|
|
55a7253dc2 | ||
|
|
e733529dc3 | ||
|
|
cc39d4ee23 | ||
|
|
0ff889c3be | ||
|
|
39589b8c02 | ||
|
|
c5c0462258 | ||
|
|
dafcba7336 | ||
|
|
19ff7bdc30 | ||
|
|
a9384e26d8 | ||
|
|
0bc190e97e | ||
|
|
2c76afb839 | ||
|
|
0edb7b77a1 | ||
|
|
59bffa66ad | ||
|
|
145c644c9d | ||
|
|
d88bb7f855 | ||
|
|
850552bf17 | ||
|
|
66a19424af | ||
|
|
a234293146 | ||
|
|
5fced70948 | ||
|
|
7a0e1818c0 | ||
|
|
bd0e5e16b8 | ||
|
|
487c664e43 | ||
|
|
fed2a429c7 | ||
|
|
ad9e709d96 | ||
|
|
4c58ea63d6 | ||
|
|
e5ec4e706a | ||
|
|
158e31d54a | ||
|
|
05820ac272 | ||
|
|
fe0d8bb7da | ||
|
|
6f74a9e3eb | ||
|
|
d90f50d589 | ||
|
|
517fc2bd75 | ||
|
|
b6316c9fcd | ||
|
|
675d948a1f | ||
|
|
49bf3f4512 | ||
|
|
3ff848b019 | ||
|
|
8b2550cef0 | ||
|
|
813f886920 | ||
|
|
c75c546888 | ||
|
|
baa41b2c13 | ||
|
|
aeaaa4a77a | ||
|
|
1025bdc76e | ||
|
|
0b7aa19ac0 | ||
|
|
cfdaddd81a | ||
|
|
fd4c2c11ad | ||
|
|
42d93f6fdb | ||
|
|
0de3f10701 | ||
|
|
7b1876d0d8 | ||
|
|
91c395d0c6 | ||
|
|
3fc3aef268 | ||
|
|
fa2e70d571 | ||
|
|
c823654041 | ||
|
|
cfa0c6d0d7 | ||
|
|
4d4319797b | ||
|
|
e90e144ebc | ||
|
|
f701adaef5 | ||
|
|
427ce17e6e | ||
|
|
3a8522e92f | ||
|
|
886e5581d8 | ||
|
|
470c9101ae | ||
|
|
09347f79c5 | ||
|
|
1d02208316 | ||
|
|
0878f514aa | ||
|
|
1b32949443 | ||
|
|
665b80f15c | ||
|
|
51528f63e9 | ||
|
|
a7e9eebfed | ||
|
|
897673b459 | ||
|
|
19f724dcd9 | ||
|
|
4ad137f1eb | ||
|
|
dab6242ff2 | ||
|
|
d47ad37791 | ||
|
|
2adedb97da | ||
|
|
7dd64d843a | ||
|
|
f30ae69c10 | ||
|
|
c871b3f948 | ||
|
|
67f5628340 | ||
|
|
fae38a107f | ||
|
|
f35b8174aa | ||
|
|
465de11c90 | ||
|
|
a7ca139e13 | ||
|
|
94a78eabe5 | ||
|
|
0c7743e786 | ||
|
|
d0f0fc787e | ||
|
|
ce3c151b8c | ||
|
|
f49d2338fd | ||
|
|
164f46e4c0 | ||
|
|
9f527718f2 | ||
|
|
ee32829cdb | ||
|
|
43cb44dd38 | ||
|
|
e8854a2675 | ||
|
|
b4c27f5d34 | ||
|
|
9dab2ba6e4 | ||
|
|
d105dd47e0 | ||
|
|
f4f2a6f5fc | ||
|
|
3542ab86e9 | ||
|
|
f76babc699 | ||
|
|
da7fec4d35 | ||
|
|
488e7b1a26 | ||
|
|
f45b27f507 | ||
|
|
796c5e8b6b | ||
|
|
89a4249072 | ||
|
|
05ffdd07a2 | ||
|
|
14fee1c086 | ||
|
|
3a7b0cacb8 | ||
|
|
ddd70fd198 | ||
|
|
fc231c5ef8 | ||
|
|
933832fe2c | ||
|
|
4ed1f6b814 | ||
|
|
9ed1d27f86 | ||
|
|
4057a3112d | ||
|
|
0b01b75cac | ||
|
|
9c635781bd | ||
|
|
b6bfeaaba3 | ||
|
|
0318a4a5e1 | ||
|
|
2d985c0c6a | ||
|
|
3ef47b0ce3 | ||
|
|
213db3b107 | ||
|
|
d475ee37c3 | ||
|
|
1d9ed1b56d | ||
|
|
e34b6a36d5 | ||
|
|
8468a74ade | ||
|
|
34cbdee510 | ||
|
|
924f6ca715 | ||
|
|
9eb24cedd6 | ||
|
|
8f6016e9ae | ||
|
|
95071c9d97 | ||
|
|
7ee7e1be5d | ||
|
|
20a6284062 | ||
|
|
5aa92f47b6 | ||
|
|
25baf7bb45 | ||
|
|
f9e045d14c | ||
|
|
4dbeb17075 | ||
|
|
5fb8ac9685 | ||
|
|
dc184601a8 | ||
|
|
64ddea5473 | ||
|
|
021bb2d6d5 | ||
|
|
644d16d82b | ||
|
|
a10eb88a95 | ||
|
|
930742ae2c | ||
|
|
482e2d5d42 | ||
|
|
034ab0378f | ||
|
|
6d911581c3 | ||
|
|
cd2368f5f3 | ||
|
|
7ed347269f | ||
|
|
068d9eef8d | ||
|
|
d0c0720578 | ||
|
|
eb0bce8dbf | ||
|
|
ae1881a68c | ||
|
|
cd97eb3fa6 | ||
|
|
a92665c5cd | ||
|
|
b2b1600ebe | ||
|
|
5a42d2a36d | ||
|
|
accf8d5c81 | ||
|
|
b35fd7e507 | ||
|
|
b2737a3d35 | ||
|
|
9ed2b4e10b | ||
|
|
2e7788b072 | ||
|
|
62f6c855bc | ||
|
|
49eb3ab2cf | ||
|
|
0f792f9eb9 | ||
|
|
ba9b7eb946 | ||
|
|
d68e2d6e15 | ||
|
|
0a66e86ccc | ||
|
|
4cadf1d43b | ||
|
|
9ffc291fcf | ||
|
|
a42d4ff6c1 | ||
|
|
f6af29fc3b | ||
|
|
b0a66cc03d | ||
|
|
d222387d01 | ||
|
|
e42aad4b2f | ||
|
|
a206a5714e | ||
|
|
8272e3ed0f | ||
|
|
9e392977b9 | ||
|
|
c77c65c68a | ||
|
|
3fe659587f | ||
|
|
6efee036a8 | ||
|
|
e6175581bd | ||
|
|
f3101a1db2 | ||
|
|
4a5bca860a | ||
|
|
b60da00028 | ||
|
|
13c444bba6 | ||
|
|
9a3669d801 | ||
|
|
18708f30d9 | ||
|
|
5193f01c8c | ||
|
|
0cc06fcba8 | ||
|
|
ac75a31641 | ||
|
|
57335c6d3a | ||
|
|
3033537236 | ||
|
|
8eeab25468 | ||
|
|
73ed5f6ee2 | ||
|
|
a6b8a34ac9 | ||
|
|
4f15cd55be | ||
|
|
dffdd3377e | ||
|
|
05735ad2c3 | ||
|
|
8fe93eae38 | ||
|
|
84747792ff | ||
|
|
91b2fe8dcb | ||
|
|
4856d57c0e | ||
|
|
63ac527a66 | ||
|
|
87a64cdacb | ||
|
|
25b763a052 | ||
|
|
903aba5dee | ||
|
|
e8d843c93d | ||
|
|
f56003e288 | ||
|
|
1509e737c2 | ||
|
|
2c286f7b60 | ||
|
|
0e7b404121 | ||
|
|
de245e00e3 | ||
|
|
ce5f9e8930 | ||
|
|
5db9f7aa0e | ||
|
|
4a9c8c6d74 | ||
|
|
680f80a833 | ||
|
|
0e6238bf6f | ||
|
|
11cdf13148 | ||
|
|
b642c3acfd | ||
|
|
200e263f1f | ||
|
|
2eb1a64d82 | ||
|
|
f3a14b6081 | ||
|
|
1e98002b8f | ||
|
|
4559eed0ec | ||
|
|
6a393ef6a2 | ||
|
|
9b8ec78502 | ||
|
|
224fe32b72 | ||
|
|
6b7566fed8 | ||
|
|
0ef28e5786 | ||
|
|
06c7f6034d | ||
|
|
e66b28fb87 | ||
|
|
6a51f081ac | ||
|
|
5536f9925a | ||
|
|
4f728c3d42 | ||
|
|
aacc36aee0 | ||
|
|
f9840c66f8 | ||
|
|
3b579900bb | ||
|
|
c73649b19b | ||
|
|
396caa52cf |
2
.gitattributes
vendored
@@ -5,7 +5,7 @@
|
|||||||
# when checked out on windows
|
# when checked out on windows
|
||||||
*.sh text eol=lf
|
*.sh text eol=lf
|
||||||
distribution/debian/* text eol=lf
|
distribution/debian/* text eol=lf
|
||||||
macOS/Sonarr text eol=lf
|
distribution/osx/Sonarr text eol=lf
|
||||||
|
|
||||||
# Custom for Visual Studio
|
# Custom for Visual Studio
|
||||||
*.cs diff=csharp
|
*.cs diff=csharp
|
||||||
|
|||||||
41
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,41 +0,0 @@
|
|||||||
<!--
|
|
||||||
Before opening a new issue, please ensure:
|
|
||||||
- You use the forums for support/questions
|
|
||||||
- You search for existing bugs/feature requests
|
|
||||||
- Remove extraneous template details
|
|
||||||
- Do not prefix title with type of issue (Feature Request, Bug, etc.) The appropriate labels will be added during triage.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Support / Questions
|
|
||||||
|
|
||||||
Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Remove if not opening a bug report
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Bug Report
|
|
||||||
|
|
||||||
### System Information/Logs
|
|
||||||
|
|
||||||
**Sonarr Version:**
|
|
||||||
|
|
||||||
**Operating System:**
|
|
||||||
|
|
||||||
**.net Framework (Windows) or mono (macOS/Linux) Version:**
|
|
||||||
|
|
||||||
**Link to Log Files (debug or trace):**
|
|
||||||
|
|
||||||
**Browser (for UI bugs):**
|
|
||||||
|
|
||||||
### Additional Information
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Remove if not opening a feature request
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Feature Request
|
|
||||||
|
|
||||||
### What problem are you looking to solve?
|
|
||||||
|
|
||||||
### Other Information
|
|
||||||
48
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,28 +1,36 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug Report
|
||||||
about: Create a report to help us improve Sonarr
|
about: Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<!-- Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug! -->
|
||||||
**Describe the bug**
|
**Describe the bug**
|
||||||
A clear and concise description of what the bug is.
|
<!-- A clear and concise description of what the bug is. -->
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
<!-- Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error -->
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
<!-- A clear and concise description of what you expected to happen.-->
|
||||||
|
|
||||||
**Screenshots**
|
**Screenshots**
|
||||||
If applicable, add screenshots to help explain your problem.
|
<!-- If applicable, add screenshots to help explain your problem.-->
|
||||||
|
|
||||||
**Logs**
|
**Platform Information (please complete the following information):**
|
||||||
Link to debug or trace log files.
|
- OS: <!-- [e.g. Windows 10 2004 / Ubuntu 20.04] -->
|
||||||
|
- Docker: <!-- [Yes/No] -->
|
||||||
|
- .net Framework (Windows) or mono (macOS/Linux) (System -> Status): <!--[e.g. Mono 5.8, Mono 6.2, .net 4.5] -->
|
||||||
|
- Browser and Version (Only needed for UI issues): <!--[e.g. chrome 86.0.4240.198] -->
|
||||||
|
- Sonarr Version: <!--[e.g. 2.0.0.5344 , 3.0.4.1077]-->
|
||||||
|
- Sonarr Branch: <!--[e.g. master, develop , phantom-develop]-->
|
||||||
|
|
||||||
**System Information**
|
**Trace Logs**
|
||||||
|
Turn on Trace logs under Settings -> General and wait for the bug to occur again.
|
||||||
- Sonarr Version: [e.g. 2.0.0.1]
|
**Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**
|
||||||
- Operating System: [e.g. Windows 10]
|
|
||||||
- .net Framework (Windows) or mono (macOS/Linux) Version: [e.g. 4.5 or 5.12]
|
|
||||||
|
|
||||||
**UI Bugs:**
|
|
||||||
- OS: [e.g. Windows]
|
|
||||||
- Browser: [e.g. chrome, firefox]
|
|
||||||
- Version: [e.g. 22]
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
14
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
blank_issues_enabled: false
|
||||||
|
contact_links:
|
||||||
|
- name: Support via Discord
|
||||||
|
url: https://discord.gg/M6BvZn5
|
||||||
|
about: Chat with users and devs on support and setup related topics.
|
||||||
|
- name: Support via Reddit
|
||||||
|
url: https://reddit.com/r/Sonarr
|
||||||
|
about: Discuss and search through support topics.
|
||||||
|
- name: Support via Forums
|
||||||
|
url: https://forums.sonarr.tv/
|
||||||
|
about: Discuss and search through support topics.
|
||||||
|
- name: Support via IRC
|
||||||
|
url: http://webchat.freenode.net/?channels=#sonarr
|
||||||
|
about: Chat with users and devs on support and setup related topics.
|
||||||
16
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,14 +1,20 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea for Sonarr
|
about: Suggest an idea for Sonarr
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
**Describe the problem**
|
**Is your feature request related to a problem? Please describe.**
|
||||||
A clear and concise description of the problem you're looking to solve.
|
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||||
|
|
||||||
**Describe any solutions you think might work**
|
**Describe the solution you'd like**
|
||||||
A clear and concise description of any solutions or features you've considered.
|
<!-- A clear and concise description of what you want to happen. -->
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context or screenshots about the feature request here.
|
<!-- Add any other context or screenshots about the feature request here. -->
|
||||||
7
.github/ISSUE_TEMPLATE/other-issues.md
vendored
@@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
name: Other issues
|
|
||||||
about: How to get support or ask questions
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.
|
|
||||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -6,7 +6,7 @@ A few sentences describing the overall goals of the pull request's commits.
|
|||||||
|
|
||||||
#### Todos
|
#### Todos
|
||||||
- [ ] Tests
|
- [ ] Tests
|
||||||
- [ ] Documentation
|
- [ ] Wiki Updates
|
||||||
|
|
||||||
|
|
||||||
#### Issues Fixed or Closed by this PR
|
#### Issues Fixed or Closed by this PR
|
||||||
|
|||||||
6
.github/SUPPORT.md
vendored
@@ -1,7 +1,7 @@
|
|||||||
## Support
|
## Support
|
||||||
|
|
||||||
There are a number of frequently asked questions that have been answered in our [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
|
There are a number of frequently asked questions that have been answered in our [FAQ](https://wiki.servarr.com/Sonarr_FAQ)
|
||||||
|
|
||||||
The [wiki](https://github.com/Sonarr/Sonarr/wiki) contains other information and guides
|
The [wiki](https://wiki.servarr.com/Sonarr) contains other information and guides
|
||||||
|
|
||||||
If you have a support question, please use the [support forums](https://forums.sonarr.tv/).
|
Please use one of the support channels: [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord ](https://discord.gg/M6BvZn5), or [IRC ](http://webchat.freenode.net/?channels=#sonarr)for support/questions.
|
||||||
21
.github/workflows/lock.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: 'Lock threads'
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lock:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/lock-threads@v2
|
||||||
|
with:
|
||||||
|
github-token: ${{ github.token }}
|
||||||
|
issue-lock-inactive-days: '90'
|
||||||
|
issue-exclude-created-before: ''
|
||||||
|
issue-exclude-labels: 'one-day-maybe'
|
||||||
|
issue-lock-labels: ''
|
||||||
|
issue-lock-comment: ''
|
||||||
|
issue-lock-reason: 'resolved'
|
||||||
|
process-only: ''
|
||||||
1
.gitignore
vendored
@@ -141,3 +141,4 @@ output/*
|
|||||||
_start
|
_start
|
||||||
|
|
||||||
src/.idea/
|
src/.idea/
|
||||||
|
/distribution/windows/setup/output/*
|
||||||
|
|||||||
@@ -3,25 +3,40 @@
|
|||||||
We're always looking for people to help make Sonarr even better, there are a number of ways to contribute.
|
We're always looking for people to help make Sonarr even better, there are a number of ways to contribute.
|
||||||
|
|
||||||
## Documentation ##
|
## Documentation ##
|
||||||
Setup guides, FAQ, the more information we have on the wiki the better.
|
Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information we have on the [wiki](https://wiki.servarr.com/Sonarr) the better.
|
||||||
|
|
||||||
## Development ##
|
## Development ##
|
||||||
|
|
||||||
See the readme for information on setting up your development environment.
|
### Tools required ###
|
||||||
|
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
|
||||||
|
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
|
||||||
|
- [Git](https://git-scm.com/downloads)
|
||||||
|
- [NodeJS](https://nodejs.org/en/download/) (Node 10.X.X or higher)
|
||||||
|
- [Yarn](https://yarnpkg.com/)
|
||||||
|
|
||||||
|
### Getting started ###
|
||||||
|
|
||||||
|
1. Fork Sonarr
|
||||||
|
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
|
||||||
|
3. Install the required Node Packages `yarn install`
|
||||||
|
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
|
||||||
|
5. Build the project in Visual Studio, Setting startup project to `Sonarr.Console` and framework to `x86`
|
||||||
|
6. Debug the project in Visual Studio
|
||||||
|
7. Open http://localhost:8989
|
||||||
|
|
||||||
### Contributing Code ###
|
### Contributing Code ###
|
||||||
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)
|
||||||
- Rebase from Sonarr's develop branch, don't merge
|
- Rebase from Sonarr's develop (currently phantom-develop) branch, don't merge
|
||||||
- Make meaningful commits, or squash them
|
- Make meaningful commits, or squash them
|
||||||
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
||||||
- Reach out to us on the forums or on IRC if you have any questions
|
- Reach out to us on our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](http://webchat.freenode.net/?channels=#sonarr) if you have any questions
|
||||||
- Add tests (unit/integration)
|
- Add tests (unit/integration)
|
||||||
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
|
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
|
||||||
- One feature/bug fix per pull request to keep things clean and easy to understand
|
- One feature/bug fix per pull request to keep things clean and easy to understand
|
||||||
- Use 4 spaces instead of tabs, this is the default for VS 2012 and WebStorm (to my knowledge)
|
- Use 4 spaces instead of tabs, this should be the default for VS 2019 and WebStorm
|
||||||
|
|
||||||
### Pull Requesting ###
|
### Pull Requesting ###
|
||||||
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
|
- Only make pull requests to develop (currently phantom-develop), never master, if you make a PR to master we'll comment on it and close it
|
||||||
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
|
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
|
||||||
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
|
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
|
||||||
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
|
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
|
||||||
|
|||||||
12
FUNDING.yml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: # Replace with a single Patreon username
|
||||||
|
open_collective: sonarr
|
||||||
|
ko_fi: # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
otechie: # Replace with a single Otechie username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
33
Logo/Jetbrains/dottrace.svg
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-1.3318" y1="43.7371" x2="67.0419" y2="26.0967">
|
||||||
|
<stop offset="0.1237" style="stop-color:#7866FF"/>
|
||||||
|
<stop offset="0.5376" style="stop-color:#FE2EB6"/>
|
||||||
|
<stop offset="0.8548" style="stop-color:#FD0486"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_1_);" points="67.3,16 43.7,0 0,31.1 11.1,70 58.9,60.3 "/>
|
||||||
|
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="45.9148" y1="38.9098" x2="67.6577" y2="9.0989">
|
||||||
|
<stop offset="0.1237" style="stop-color:#FF0080"/>
|
||||||
|
<stop offset="0.2587" style="stop-color:#FE0385"/>
|
||||||
|
<stop offset="0.4109" style="stop-color:#FA0C92"/>
|
||||||
|
<stop offset="0.5713" style="stop-color:#F41BA9"/>
|
||||||
|
<stop offset="0.7363" style="stop-color:#EB2FC8"/>
|
||||||
|
<stop offset="0.8656" style="stop-color:#E343E6"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_2_);" points="67.3,16 43.7,0 38,15.7 38,47.8 70,47.8 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
||||||
|
<rect x="17.4" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
||||||
|
<g>
|
||||||
|
<path style="fill:#FFFFFF;" d="M17.4,19.1h6.9c5.6,0,9.5,3.8,9.5,8.9V28c0,5-3.9,8.9-9.5,8.9h-6.9V19.1z M21.4,22.7v10.7h3
|
||||||
|
c3.2,0,5.4-2.2,5.4-5.3V28c0-3.2-2.2-5.4-5.4-5.4H21.4z"/>
|
||||||
|
<polygon style="fill:#FFFFFF;" points="40.3,22.7 34.9,22.7 34.9,19.1 49.6,19.1 49.6,22.7 44.2,22.7 44.2,37 40.3,37 "/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
66
Logo/Jetbrains/jetbrains.svg
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="120.1px" height="130.2px" viewBox="0 0 120.1 130.2" style="enable-background:new 0 0 120.1 130.2;" xml:space="preserve"
|
||||||
|
>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="XMLID_2_" gradientUnits="userSpaceOnUse" x1="31.8412" y1="120.5578" x2="110.2402" y2="73.24">
|
||||||
|
<stop offset="0" style="stop-color:#FCEE39"/>
|
||||||
|
<stop offset="1" style="stop-color:#F37B3D"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path id="XMLID_3041_" style="fill:url(#XMLID_2_);" d="M118.6,71.8c0.9-0.8,1.4-1.9,1.5-3.2c0.1-2.6-1.8-4.7-4.4-4.9
|
||||||
|
c-1.2-0.1-2.4,0.4-3.3,1.1l0,0l-83.8,45.9c-1.9,0.8-3.6,2.2-4.7,4.1c-2.9,4.8-1.3,11,3.6,13.9c3.4,2,7.5,1.8,10.7-0.2l0,0l0,0
|
||||||
|
c0.2-0.2,0.5-0.3,0.7-0.5l78-54.8C117.3,72.9,118.4,72.1,118.6,71.8L118.6,71.8L118.6,71.8z"/>
|
||||||
|
<linearGradient id="XMLID_3_" gradientUnits="userSpaceOnUse" x1="48.3607" y1="6.9083" x2="119.9179" y2="69.5546">
|
||||||
|
<stop offset="0" style="stop-color:#EF5A6B"/>
|
||||||
|
<stop offset="0.57" style="stop-color:#F26F4E"/>
|
||||||
|
<stop offset="1" style="stop-color:#F37B3D"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path id="XMLID_3049_" style="fill:url(#XMLID_3_);" d="M118.8,65.1L118.8,65.1L55,2.5C53.6,1,51.6,0,49.3,0
|
||||||
|
c-4.3,0-7.7,3.5-7.7,7.7v0c0,2.1,0.8,3.9,2.1,5.3l0,0l0,0c0.4,0.4,0.8,0.7,1.2,1l67.4,57.7l0,0c0.8,0.7,1.8,1.2,3,1.3
|
||||||
|
c2.6,0.1,4.7-1.8,4.9-4.4C120.2,67.3,119.7,66,118.8,65.1z"/>
|
||||||
|
<linearGradient id="XMLID_4_" gradientUnits="userSpaceOnUse" x1="52.9467" y1="63.6407" x2="10.5379" y2="37.1562">
|
||||||
|
<stop offset="0" style="stop-color:#7C59A4"/>
|
||||||
|
<stop offset="0.3852" style="stop-color:#AF4C92"/>
|
||||||
|
<stop offset="0.7654" style="stop-color:#DC4183"/>
|
||||||
|
<stop offset="0.957" style="stop-color:#ED3D7D"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path id="XMLID_3042_" style="fill:url(#XMLID_4_);" d="M57.1,59.5C57,59.5,17.7,28.5,16.9,28l0,0l0,0c-0.6-0.3-1.2-0.6-1.8-0.9
|
||||||
|
c-5.8-2.2-12.2,0.8-14.4,6.6c-1.9,5.1,0.2,10.7,4.6,13.4l0,0l0,0C6,47.5,6.6,47.8,7.3,48c0.4,0.2,45.4,18.8,45.4,18.8l0,0
|
||||||
|
c1.8,0.8,3.9,0.3,5.1-1.2C59.3,63.7,59,61,57.1,59.5z"/>
|
||||||
|
<linearGradient id="XMLID_5_" gradientUnits="userSpaceOnUse" x1="52.1736" y1="3.7019" x2="10.7706" y2="37.8971">
|
||||||
|
<stop offset="0" style="stop-color:#EF5A6B"/>
|
||||||
|
<stop offset="0.364" style="stop-color:#EE4E72"/>
|
||||||
|
<stop offset="1" style="stop-color:#ED3D7D"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path id="XMLID_3057_" style="fill:url(#XMLID_5_);" d="M49.3,0c-1.7,0-3.3,0.6-4.6,1.5L4.9,28.3c-0.1,0.1-0.2,0.1-0.2,0.2l-0.1,0
|
||||||
|
l0,0c-1.7,1.2-3.1,3-3.9,5.1C-1.5,39.4,1.5,45.9,7.3,48c3.6,1.4,7.5,0.7,10.4-1.4l0,0l0,0c0.7-0.5,1.3-1,1.8-1.6l34.6-31.2l0,0
|
||||||
|
c1.8-1.4,3-3.6,3-6.1v0C57.1,3.5,53.6,0,49.3,0z"/>
|
||||||
|
<g id="XMLID_3008_">
|
||||||
|
<rect id="XMLID_3033_" x="34.6" y="37.4" style="fill:#000000;" width="51" height="51"/>
|
||||||
|
<rect id="XMLID_3032_" x="39" y="78.8" style="fill:#FFFFFF;" width="19.1" height="3.2"/>
|
||||||
|
<g id="XMLID_3009_">
|
||||||
|
<path id="XMLID_3030_" style="fill:#FFFFFF;" d="M38.8,50.8l1.5-1.4c0.4,0.5,0.8,0.8,1.3,0.8c0.6,0,0.9-0.4,0.9-1.2l0-5.3l2.3,0
|
||||||
|
l0,5.3c0,1-0.3,1.8-0.8,2.3c-0.5,0.5-1.3,0.8-2.3,0.8C40.2,52.2,39.4,51.6,38.8,50.8z"/>
|
||||||
|
<path id="XMLID_3028_" style="fill:#FFFFFF;" d="M45.3,43.8l6.7,0v1.9l-4.4,0V47l4,0l0,1.8l-4,0l0,1.3l4.5,0l0,2l-6.7,0
|
||||||
|
L45.3,43.8z"/>
|
||||||
|
<path id="XMLID_3026_" style="fill:#FFFFFF;" d="M55,45.8l-2.5,0l0-2l7.3,0l0,2l-2.5,0l0,6.3l-2.3,0L55,45.8z"/>
|
||||||
|
<path id="XMLID_3022_" style="fill:#FFFFFF;" d="M39,54l4.3,0c1,0,1.8,0.3,2.3,0.7c0.3,0.3,0.5,0.8,0.5,1.4v0
|
||||||
|
c0,1-0.5,1.5-1.3,1.9c1,0.3,1.6,0.9,1.6,2v0c0,1.4-1.2,2.3-3.1,2.3l-4.3,0L39,54z M43.8,56.6c0-0.5-0.4-0.7-1-0.7l-1.5,0l0,1.5
|
||||||
|
l1.4,0C43.4,57.3,43.8,57.1,43.8,56.6L43.8,56.6z M43,59l-1.8,0l0,1.5H43c0.7,0,1.1-0.3,1.1-0.8v0C44.1,59.2,43.7,59,43,59z"/>
|
||||||
|
<path id="XMLID_3019_" style="fill:#FFFFFF;" d="M46.8,54l3.9,0c1.3,0,2.1,0.3,2.7,0.9c0.5,0.5,0.7,1.1,0.7,1.9v0
|
||||||
|
c0,1.3-0.7,2.1-1.7,2.6l2,2.9l-2.6,0l-1.7-2.5h-1l0,2.5l-2.3,0L46.8,54z M50.6,58c0.8,0,1.2-0.4,1.2-1v0c0-0.7-0.5-1-1.2-1
|
||||||
|
l-1.5,0v2H50.6z"/>
|
||||||
|
<path id="XMLID_3016_" style="fill:#FFFFFF;" d="M56.8,54l2.2,0l3.5,8.4l-2.5,0l-0.6-1.5l-3.2,0l-0.6,1.5l-2.4,0L56.8,54z
|
||||||
|
M58.8,59l-0.9-2.3L57,59L58.8,59z"/>
|
||||||
|
<path id="XMLID_3014_" style="fill:#FFFFFF;" d="M62.8,54l2.3,0l0,8.3l-2.3,0L62.8,54z"/>
|
||||||
|
<path id="XMLID_3012_" style="fill:#FFFFFF;" d="M65.7,54l2.1,0l3.4,4.4l0-4.4l2.3,0l0,8.3l-2,0L68,57.8l0,4.6l-2.3,0L65.7,54z"
|
||||||
|
/>
|
||||||
|
<path id="XMLID_3010_" style="fill:#FFFFFF;" d="M73.7,61.1l1.3-1.5c0.8,0.7,1.7,1,2.7,1c0.6,0,1-0.2,1-0.6v0
|
||||||
|
c0-0.4-0.3-0.5-1.4-0.8c-1.8-0.4-3.1-0.9-3.1-2.6v0c0-1.5,1.2-2.7,3.2-2.7c1.4,0,2.5,0.4,3.4,1.1l-1.2,1.6
|
||||||
|
c-0.8-0.5-1.6-0.8-2.3-0.8c-0.6,0-0.8,0.2-0.8,0.5v0c0,0.4,0.3,0.5,1.4,0.8c1.9,0.4,3.1,1,3.1,2.6v0c0,1.7-1.3,2.7-3.4,2.7
|
||||||
|
C76.1,62.5,74.7,62,73.7,61.1z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.8 KiB |
50
Logo/Jetbrains/resharper.svg
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
width="70px" height="70px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="22.9451" y1="75.7869" x2="74.7868" y2="20.6415">
|
||||||
|
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
||||||
|
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
||||||
|
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
||||||
|
<stop offset="0.6505" style="stop-color:#EB8523"/>
|
||||||
|
<stop offset="0.9516" style="stop-color:#FEBD11"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_1_);" points="49.8,15.2 36,36.7 58.4,70 70,23.1 "/>
|
||||||
|
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="17.7187" y1="73.2922" x2="69.5556" y2="18.1519">
|
||||||
|
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
||||||
|
<stop offset="0.4044" style="stop-color:#C41E57"/>
|
||||||
|
<stop offset="0.4677" style="stop-color:#C41E57"/>
|
||||||
|
<stop offset="0.7043" style="stop-color:#EB8523"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_2_);" points="51.1,15.7 49,0 18.8,33.6 27.6,42.3 20.8,70 58.4,70 "/>
|
||||||
|
</g>
|
||||||
|
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="1.8281" y1="53.4275" x2="48.8245" y2="9.2255">
|
||||||
|
<stop offset="1.612903e-002" style="stop-color:#B35BA3"/>
|
||||||
|
<stop offset="0.6613" style="stop-color:#C41E57"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_3_);" points="49,0 11.6,0 0,47.1 55.6,47.1 "/>
|
||||||
|
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="49.8935" y1="-11.5569" x2="48.8588" y2="24.0352">
|
||||||
|
<stop offset="0.5" style="stop-color:#C41E57"/>
|
||||||
|
<stop offset="0.6668" style="stop-color:#D13F48"/>
|
||||||
|
<stop offset="0.7952" style="stop-color:#D94F39"/>
|
||||||
|
<stop offset="0.8656" style="stop-color:#DD5433"/>
|
||||||
|
</linearGradient>
|
||||||
|
<polygon style="fill:url(#SVGID_4_);" points="55.3,47.1 51.1,15.7 49,0 41.7,23 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
|
||||||
|
<rect x="13.4" y="13.5" transform="matrix(-1 2.577289e-003 -2.577289e-003 -1 70.0288 70.081)" style="fill:#000000;" width="43.2" height="43.2"/>
|
||||||
|
|
||||||
|
<rect x="17.6" y="48.6" transform="matrix(1 -2.577289e-003 2.577289e-003 1 -0.1287 6.634109e-002)" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M17.4,19.1l8.2,0c2.3,0,4,0.6,5.2,1.8c1,1,1.5,2.4,1.5,4.1l0,0.1c0,1.5-0.3,2.6-1.1,3.5
|
||||||
|
c-0.7,0.9-1.6,1.6-2.8,2l4.4,6.4l-4.6,0l-3.7-5.5l-3.3,0l0,5.5l-3.9,0L17.4,19.1z M25.3,27.8c1,0,1.7-0.2,2.2-0.7
|
||||||
|
c0.5-0.5,0.8-1.1,0.8-1.8l0-0.1c0-0.9-0.3-1.5-0.8-1.9c-0.5-0.4-1.3-0.6-2.3-0.6l-3.9,0l0,5.1L25.3,27.8z"/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M36,33.2l-1.9,0l0-3.3l2.5,0l0.6-3.8l-2.3,0l0-3.3l2.8,0l0.6-3.7l3.4,0l-0.6,3.7l3.7,0l0.6-3.7
|
||||||
|
l3.4,0l-0.6,3.7l1.9,0l0,3.3l-2.5,0L47,29.9l2.3,0l0,3.3l-2.8,0L45.8,37l-3.4,0l0.7-3.8l-3.7,0L38.7,37l-3.4,0L36,33.2z
|
||||||
|
M43.7,29.9l0.6-3.8l-3.7,0L40,29.9L43.7,29.9z"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
64
Logo/Jetbrains/teamcity.svg
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 70 70" style="enable-background:new 0 0 70 70;" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="1.7738" y1="31.2729" x2="40.1662" y2="31.2729">
|
||||||
|
<stop offset="0" style="stop-color:#905CFB"/>
|
||||||
|
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
||||||
|
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
||||||
|
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
||||||
|
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
||||||
|
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
||||||
|
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
||||||
|
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_1_);" d="M39.7,47.9l-6.1-34c-0.4-2.4-1.2-4.8-2.7-7.1c-2-3.2-5.2-5.4-8.8-6.3
|
||||||
|
C7.9-2.9-2.6,11.3,3.6,23.9c0,0,0,0,0,0l14.8,31.7c0.4,1,1,2,1.7,2.9c1.2,1.6,2.8,2.8,4.7,3.4C34.4,64.9,42.1,56.4,39.7,47.9z"/>
|
||||||
|
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="5.3113" y1="9.6691" x2="69.2278" y2="43.8664">
|
||||||
|
<stop offset="0" style="stop-color:#905CFB"/>
|
||||||
|
<stop offset="6.772543e-002" style="stop-color:#776CF9"/>
|
||||||
|
<stop offset="0.1729" style="stop-color:#5681F7"/>
|
||||||
|
<stop offset="0.2865" style="stop-color:#3B92F5"/>
|
||||||
|
<stop offset="0.4097" style="stop-color:#269FF4"/>
|
||||||
|
<stop offset="0.5474" style="stop-color:#17A9F3"/>
|
||||||
|
<stop offset="0.7111" style="stop-color:#0FAEF2"/>
|
||||||
|
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_2_);" d="M67.4,26.5c-1.4-2.2-3.4-3.9-5.7-4.9L25.5,1.7l0,0c-1-0.5-2.1-1-3.3-1.3
|
||||||
|
C6.7-3.2-4.4,13.8,5.5,27c1.5,2,3.6,3.6,6,4.5L48,47.9c0.8,0.5,1.6,0.8,2.5,1.1C64.5,53.4,75.1,38.6,67.4,26.5z"/>
|
||||||
|
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-19.2836" y1="70.8198" x2="55.9833" y2="33.1863">
|
||||||
|
<stop offset="0" style="stop-color:#3BEA62"/>
|
||||||
|
<stop offset="0.117" style="stop-color:#31DE80"/>
|
||||||
|
<stop offset="0.3025" style="stop-color:#24CEA8"/>
|
||||||
|
<stop offset="0.4844" style="stop-color:#1AC1C9"/>
|
||||||
|
<stop offset="0.6592" style="stop-color:#12B7DF"/>
|
||||||
|
<stop offset="0.8238" style="stop-color:#0EB2ED"/>
|
||||||
|
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_3_);" d="M67.4,26.5c-1.8-2.8-4.6-4.8-7.9-5.6c-3.5-0.8-6.8-0.5-9.6,0.7L11.4,36.1
|
||||||
|
c0,0-0.2,0.1-0.6,0.4C0.9,40.4-4,53.3,4,64c1.8,2.4,4.3,4.2,7.1,5c5.3,1.6,10.1,1,14-1.1c0,0,0.1,0,0.1,0l37.6-20.1
|
||||||
|
c0,0,0,0,0.1-0.1C69.5,43.9,72.6,34.6,67.4,26.5z"/>
|
||||||
|
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="38.9439" y1="5.8503" x2="5.4232" y2="77.5093">
|
||||||
|
<stop offset="0" style="stop-color:#3BEA62"/>
|
||||||
|
<stop offset="9.397750e-002" style="stop-color:#2FDB87"/>
|
||||||
|
<stop offset="0.196" style="stop-color:#24CEA8"/>
|
||||||
|
<stop offset="0.3063" style="stop-color:#1BC3C3"/>
|
||||||
|
<stop offset="0.4259" style="stop-color:#14BAD8"/>
|
||||||
|
<stop offset="0.5596" style="stop-color:#10B5E7"/>
|
||||||
|
<stop offset="0.7185" style="stop-color:#0DB1EF"/>
|
||||||
|
<stop offset="0.9677" style="stop-color:#0CB0F2"/>
|
||||||
|
</linearGradient>
|
||||||
|
<path style="fill:url(#SVGID_4_);" d="M50.3,12.8c1.2-2.7,1.1-6-0.9-9c-1.1-1.8-2.9-3-4.9-3.5c-4.5-1.1-8.3,1-10.1,4.2L3.5,42
|
||||||
|
c0,0,0,0,0,0.1C-0.9,47.9-1.6,56.5,4,64c1.8,2.4,4.3,4.2,7.1,5c10.5,3.3,19.3-2.5,22.1-10.8L50.3,12.8z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<rect x="13.4" y="13.4" style="fill:#000000;" width="43.2" height="43.2"/>
|
||||||
|
<rect x="17.5" y="48.5" style="fill:#FFFFFF;" width="16.2" height="2.7"/>
|
||||||
|
<polygon style="fill:#FFFFFF;" points="22.9,22.7 17.5,22.7 17.5,19.1 32.3,19.1 32.3,22.7 26.8,22.7 26.8,37 22.9,37 "/>
|
||||||
|
<path style="fill:#FFFFFF;" d="M32.5,28.1L32.5,28.1c0-5.1,3.8-9.3,9.3-9.3c3.4,0,5.4,1.1,7.1,2.8l-2.5,2.9c-1.4-1.3-2.8-2-4.6-2
|
||||||
|
c-3,0-5.2,2.5-5.2,5.6V28c0,3.1,2.1,5.6,5.2,5.6c2,0,3.3-0.8,4.7-2.1l2.5,2.5c-1.8,2-3.9,3.2-7.3,3.2
|
||||||
|
C36.4,37.3,32.5,33.2,32.5,28.1"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
68
README.md
@@ -4,17 +4,23 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
|
|||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
|
- [Download/Installation](https://sonarr.tv/#downloads-v3)
|
||||||
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
|
- [FAQ](https://wiki.servarr.com/Sonarr_FAQ)
|
||||||
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
|
- [Wiki](https://wiki.servarr.com/Sonarr)
|
||||||
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
|
- [(WIP) API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
|
||||||
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
|
- [Donate](https://sonarr.tv/donate)
|
||||||
|
|
||||||
## Support
|
## Support
|
||||||
|
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||||
|
|
||||||
- [Donate](https://sonarr.tv/donate)
|
- [Forums](https://forums.sonarr.tv/)
|
||||||
- [Discord](https://discord.gg/M6BvZn5)
|
- [Discord](https://discord.gg/M6BvZn5)
|
||||||
|
- [GitHub - Bugs and Feature Requests Only](https://github.com/Sonarr/Sonarr/issues)
|
||||||
|
- [IRC ](http://webchat.freenode.net/?channels=#sonarr)
|
||||||
- [Reddit](https://www.reddit.com/r/sonarr)
|
- [Reddit](https://www.reddit.com/r/sonarr)
|
||||||
|
- [Wiki](https://wiki.servarr.com/Sonarr)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -32,42 +38,38 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
|
|||||||
- Full support for specials and multi-episode releases
|
- Full support for specials and multi-episode releases
|
||||||
- And a beautiful UI
|
- And a beautiful UI
|
||||||
|
|
||||||
## Configuring Development Environment:
|
## Contributing
|
||||||
|
|
||||||
### Requirements
|
### Development
|
||||||
|
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||||
|
<a href="https://github.com/Sonarr/Sonarr/graphs/contributors"><img src="https://opencollective.com/Sonarr/contributors.svg?width=890&button=false" /></a>
|
||||||
|
|
||||||
- [Visual Studio 2017](https://www.visualstudio.com/vs)
|
### Supporters
|
||||||
- [Git](https://git-scm.com/downloads)
|
|
||||||
- [NodeJS](https://nodejs.org/en/download)
|
|
||||||
- [Yarn](https://yarnpkg.com)
|
|
||||||
|
|
||||||
### Setup
|
This project would not be possible without the support of our users and software providers.
|
||||||
|
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
|
||||||
|
|
||||||
- Make sure all the required software mentioned above are installed
|
#### Sponsors
|
||||||
- Clone the repository recursively to get Sonarr and it's submodules
|
|
||||||
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
|
|
||||||
- Install the required Node Packages using `yarn`
|
|
||||||
|
|
||||||
### Backend Development
|
[](https://opencollective.com/sonarr/contribute/sponsor-21443/checkout)
|
||||||
|
|
||||||
- Run `yarn build` to build the UI
|
#### Flexible Sponsors
|
||||||
- Open `Sonarr.sln` in Visual Studio
|
|
||||||
- Make sure `Sonarr.Console` is set as the startup project
|
|
||||||
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
|
|
||||||
- Build Solution
|
|
||||||
|
|
||||||
### UI Development
|
[](https://opencollective.com/sonarr/contribute/flexible-sponsor-21457/checkout)
|
||||||
|
|
||||||
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
#### Backers
|
||||||
- Run Sonarr.Console.exe (or debug in Visual Studio)
|
|
||||||
|
[](https://opencollective.com/sonarr/contribute/backer-21442/checkout)
|
||||||
|
|
||||||
|
#### JetBrains
|
||||||
|
|
||||||
|
Thank you to [<img src="/Logo/Jetbrains/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||||
|
|
||||||
|
* [<img src="/Logo/Jetbrains/teamcity.svg" alt="TeamCity" width="32"> TeamCity](http://www.jetbrains.com/teamcity/)
|
||||||
|
* [<img src="/Logo/Jetbrains/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||||
|
* [<img src="/Logo/Jetbrains/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||||
|
|
||||||
### Licenses
|
### 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-2021
|
||||||
|
|
||||||
### Sponsors
|
|
||||||
|
|
||||||
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
|
||||||
- [ReSharper](http://www.jetbrains.com/resharper/)
|
|
||||||
- [TeamCity](http://www.jetbrains.com/teamcity/)
|
|
||||||
|
|||||||
39
build.sh
@@ -266,14 +266,24 @@ PackageMacOS()
|
|||||||
|
|
||||||
rm -rf $outputFolderMacOS
|
rm -rf $outputFolderMacOS
|
||||||
mkdir $outputFolderMacOS
|
mkdir $outputFolderMacOS
|
||||||
|
|
||||||
echo "Adding Startup script"
|
|
||||||
cp ./macOS/Sonarr $outputFolderMacOS
|
|
||||||
dos2unix $outputFolderMacOS/Sonarr
|
|
||||||
|
|
||||||
echo "Copying Binaries"
|
echo "Copying Binaries"
|
||||||
cp -r $outputFolderLinux/* $outputFolderMacOS
|
cp -r $outputFolderLinux/* $outputFolderMacOS
|
||||||
|
|
||||||
|
echo "Adding Sonarr Launcher"
|
||||||
|
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/
|
||||||
|
mv $outputFolderMacOS/Sonarr.exe $outputFolderMacOS/Sonarr.exe.bak
|
||||||
|
mv $outputFolderMacOS/Launcher $outputFolderMacOS/Sonarr
|
||||||
|
mv $outputFolderMacOS/Sonarr.exe.bak $outputFolderMacOS/Sonarr.exe
|
||||||
|
chmod +x $outputFolderMacOS/Sonarr
|
||||||
|
|
||||||
|
echo "Adding Sonarr.Update Launcher"
|
||||||
|
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOS/Sonarr.Update/
|
||||||
|
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak
|
||||||
|
mv $outputFolderMacOS/Sonarr.Update/Launcher $outputFolderMacOS/Sonarr.Update/Sonarr.Update
|
||||||
|
mv $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe.bak $outputFolderMacOS/Sonarr.Update/Sonarr.Update.exe
|
||||||
|
chmod +x $outputFolderMacOS/Sonarr.Update/Sonarr.Update
|
||||||
|
|
||||||
echo "Adding sqlite dylibs"
|
echo "Adding sqlite dylibs"
|
||||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
|
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
|
||||||
|
|
||||||
@@ -289,24 +299,27 @@ PackageMacOSApp()
|
|||||||
|
|
||||||
rm -rf $outputFolderMacOSApp
|
rm -rf $outputFolderMacOSApp
|
||||||
mkdir $outputFolderMacOSApp
|
mkdir $outputFolderMacOSApp
|
||||||
cp -r ./macOS/Sonarr.app $outputFolderMacOSApp
|
cp -r ./distribution/osx/Sonarr.app $outputFolderMacOSApp
|
||||||
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
||||||
|
|
||||||
echo "Adding Startup script"
|
echo "Adding Sonarr Launcher"
|
||||||
cp ./macOS/Sonarr $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
cp ./distribution/osx/Launcher/dist/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/
|
||||||
dos2unix $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
|
mv $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Launcher $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
|
||||||
|
chmod +x $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr
|
||||||
|
|
||||||
echo "Copying Binaries"
|
echo "Copying Binaries"
|
||||||
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
mkdir -p $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin
|
||||||
|
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
|
||||||
|
|
||||||
echo "Adding sqlite dylibs"
|
echo "Adding sqlite dylibs"
|
||||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
|
||||||
|
|
||||||
echo "Adding MediaInfo dylib"
|
echo "Adding MediaInfo dylib"
|
||||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/
|
||||||
|
|
||||||
echo "Removing Update Folder"
|
echo "Removing Update Folder"
|
||||||
rm -r $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr.Update
|
rm -r $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/bin/Sonarr.Update
|
||||||
|
echo "# Do Not Edit\nPackageVersion=${BUILD_NUMBER}\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=${BUILD_NUMBER}\nUpdateMethod=$PackageUpdater\nBranch=${Branch:-master}" > $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/package_info
|
||||||
|
|
||||||
ProgressEnd 'Creating macOS App Package'
|
ProgressEnd 'Creating macOS App Package'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
fromdos ./debian/*
|
fromdos ./debian/*
|
||||||
|
chmod ugo-x ./debian/*
|
||||||
cp -r ./debian ./debian_backup
|
cp -r ./debian ./debian_backup
|
||||||
|
|
||||||
BuildVersion=${dependent_build_number:-3.10.0.999}
|
BuildVersion=${dependent_build_number:-3.10.0.999}
|
||||||
@@ -16,7 +17,7 @@ echo Updating changelog for $BuildVersion
|
|||||||
sed -i "s:{version}:$BuildVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
sed -i "s:{version}:$BuildVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$PackageUpdater:g" debian/preinst debian/postinst debian/postrm
|
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$PackageUpdater:g" debian/preinst debian/postinst debian/postrm
|
||||||
sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
sed -i '/#BEGIN BUILTIN UPDATER/,/#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
||||||
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
echo "# Do Not Edit\nPackageVersion=$BuildVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$PackageUpdater\nBranch=$BuildBranch" > package_info
|
||||||
|
|
||||||
echo Running debuild for $BuildVersion
|
echo Running debuild for $BuildVersion
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
if [ -z "${TEST_OUTPUT}" ]; then
|
||||||
@@ -33,7 +34,7 @@ echo Updating changelog for $BootstrapVersion
|
|||||||
sed -i "s:{version}:$BootstrapVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
sed -i "s:{version}:$BootstrapVersion:g; s:{branch}:$BuildBranch:g;" debian/changelog
|
||||||
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$BootstrapUpdater:g" debian/preinst debian/postinst debian/postrm
|
sed -i "s:{version}:$BuildVersion:g; s:{updater}:$BootstrapUpdater:g" debian/preinst debian/postinst debian/postrm
|
||||||
sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
sed -i '/#BEGIN BUILTIN UPDATER/d; /#END BUILTIN UPDATER/d' debian/preinst debian/postinst debian/postrm
|
||||||
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
echo "# Do Not Edit\nPackageVersion=$BootstrapVersion\nPackageAuthor=[Team Sonarr](https://sonarr.tv)\nReleaseVersion=$BuildVersion\nUpdateMethod=$BootstrapUpdater\nBranch=$BuildBranch" > package_info
|
||||||
|
|
||||||
echo Running debuild for $BootstrapVersion
|
echo Running debuild for $BootstrapVersion
|
||||||
if [ -z "${TEST_OUTPUT}" ]; then
|
if [ -z "${TEST_OUTPUT}" ]; then
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
8
|
10
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ UPDATER={updater}
|
|||||||
# Existing nzbdrone packages do not have startup scripts and the process might still be running.
|
# Existing nzbdrone packages do not have startup scripts and the process might still be running.
|
||||||
# If the user manually installed nzbdrone then the process might still be running too.
|
# If the user manually installed nzbdrone then the process might still be running too.
|
||||||
if [ $1 = "install" ]; then
|
if [ $1 = "install" ]; then
|
||||||
psNzbDrone=`ps ax -o'user,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
|
psNzbDrone=`ps ax -o'user:20,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
|
||||||
if [ ! -z "$psNzbDrone" ]; then
|
if [ ! -z "$psNzbDrone" ]; then
|
||||||
# Get the user and optional systemd unit
|
# Get the user and optional systemd unit
|
||||||
psNzbDroneUser=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 1`
|
psNzbDroneUser=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 1`
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
# Uncomment this to turn on verbose mode.
|
# Uncomment this to turn on verbose mode.
|
||||||
#export DH_VERBOSE=1
|
#export DH_VERBOSE=1
|
||||||
|
|
||||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
|
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0
|
||||||
|
|
||||||
%:
|
%:
|
||||||
dh $@ --with=systemd --with=cli
|
dh $@ --with=systemd --with=cli
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
ignores msbuild
|
ignores msbuild
|
||||||
ignores libmediainfo0v5
|
ignores libmediainfo0v5
|
||||||
|
ignores libc6
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
FROM ubuntu:xenial AS builder
|
FROM ubuntu:focal AS builder
|
||||||
|
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
ENV MONO_VERSION 5.18
|
ENV MONO_VERSION 5.18
|
||||||
|
|
||||||
|
RUN apt-get update && \
|
||||||
|
apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
|
||||||
|
apt-transport-https \
|
||||||
|
wget dirmngr gpg gpg-agent \
|
||||||
|
# add-apt-repository for PPAs
|
||||||
|
software-properties-common && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
|
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 && \
|
echo "deb http://download.mono-project.com/repo/debian stable-focal main" > /etc/apt/sources.list.d/mono-official-stable.list && \
|
||||||
apt-get update && apt-get install -y \
|
apt-get update && apt-get install -y \
|
||||||
devscripts build-essential tofrodos \
|
devscripts build-essential tofrodos \
|
||||||
dh-make dh-systemd \
|
dh-make dh-systemd \
|
||||||
cli-common-dev \
|
cli-common-dev \
|
||||||
mono-complete \
|
mono-complete \
|
||||||
sqlite3 libcurl3 mediainfo
|
sqlite3 libcurl4 mediainfo
|
||||||
|
RUN apt-get upgrade -y
|
||||||
|
|
||||||
RUN apt-cache policy mono-complete
|
RUN apt-cache policy mono-complete
|
||||||
RUN apt-cache policy cli-common-dev
|
RUN apt-cache policy cli-common-dev
|
||||||
|
|||||||
459
distribution/osx/Launcher/LICENSE.LGPL.md
Normal file
@@ -0,0 +1,459 @@
|
|||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
Version 2.1, February 1999
|
||||||
|
|
||||||
|
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
[This is the first released version of the Lesser GPL. It also counts
|
||||||
|
as the successor of the GNU Library Public License, version 2, hence
|
||||||
|
the version number 2.1.]
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
Licenses are intended to guarantee your freedom to share and change
|
||||||
|
free software--to make sure the software is free for all its users.
|
||||||
|
|
||||||
|
This license, the Lesser General Public License, applies to some
|
||||||
|
specially designated software packages--typically libraries--of the
|
||||||
|
Free Software Foundation and other authors who decide to use it. You
|
||||||
|
can use it too, but we suggest you first think carefully about whether
|
||||||
|
this license or the ordinary General Public License is the better
|
||||||
|
strategy to use in any particular case, based on the explanations below.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom of use,
|
||||||
|
not price. Our General Public Licenses are designed to make sure that
|
||||||
|
you have the freedom to distribute copies of free software (and charge
|
||||||
|
for this service if you wish); that you receive source code or can get
|
||||||
|
it if you want it; that you can change the software and use pieces of
|
||||||
|
it in new free programs; and that you are informed that you can do
|
||||||
|
these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
distributors to deny you these rights or to ask you to surrender these
|
||||||
|
rights. These restrictions translate to certain responsibilities for
|
||||||
|
you if you distribute copies of the library or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of the library, whether gratis
|
||||||
|
or for a fee, you must give the recipients all the rights that we gave
|
||||||
|
you. You must make sure that they, too, receive or can get the source
|
||||||
|
code. If you link other code with the library, you must provide
|
||||||
|
complete object files to the recipients, so that they can relink them
|
||||||
|
with the library after making changes to the library and recompiling
|
||||||
|
it. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with a two-step method: (1) we copyright the
|
||||||
|
library, and (2) we offer you this license, which gives you legal
|
||||||
|
permission to copy, distribute and/or modify the library.
|
||||||
|
|
||||||
|
To protect each distributor, we want to make it very clear that
|
||||||
|
there is no warranty for the free library. Also, if the library is
|
||||||
|
modified by someone else and passed on, the recipients should know
|
||||||
|
that what they have is not the original version, so that the original
|
||||||
|
author's reputation will not be affected by problems that might be
|
||||||
|
introduced by others.
|
||||||
|
|
||||||
|
Finally, software patents pose a constant threat to the existence of
|
||||||
|
any free program. We wish to make sure that a company cannot
|
||||||
|
effectively restrict the users of a free program by obtaining a
|
||||||
|
restrictive license from a patent holder. Therefore, we insist that
|
||||||
|
any patent license obtained for a version of the library must be
|
||||||
|
consistent with the full freedom of use specified in this license.
|
||||||
|
|
||||||
|
Most GNU software, including some libraries, is covered by the
|
||||||
|
ordinary GNU General Public License. This license, the GNU Lesser
|
||||||
|
General Public License, applies to certain designated libraries, and
|
||||||
|
is quite different from the ordinary General Public License. We use
|
||||||
|
this license for certain libraries in order to permit linking those
|
||||||
|
libraries into non-free programs.
|
||||||
|
|
||||||
|
When a program is linked with a library, whether statically or using
|
||||||
|
a shared library, the combination of the two is legally speaking a
|
||||||
|
combined work, a derivative of the original library. The ordinary
|
||||||
|
General Public License therefore permits such linking only if the
|
||||||
|
entire combination fits its criteria of freedom. The Lesser General
|
||||||
|
Public License permits more lax criteria for linking other code with
|
||||||
|
the library.
|
||||||
|
|
||||||
|
We call this license the "Lesser" General Public License because it
|
||||||
|
does Less to protect the user's freedom than the ordinary General
|
||||||
|
Public License. It also provides other free software developers Less
|
||||||
|
of an advantage over competing non-free programs. These disadvantages
|
||||||
|
are the reason we use the ordinary General Public License for many
|
||||||
|
libraries. However, the Lesser license provides advantages in certain
|
||||||
|
special circumstances.
|
||||||
|
|
||||||
|
For example, on rare occasions, there may be a special need to
|
||||||
|
encourage the widest possible use of a certain library, so that it becomes
|
||||||
|
a de-facto standard. To achieve this, non-free programs must be
|
||||||
|
allowed to use the library. A more frequent case is that a free
|
||||||
|
library does the same job as widely used non-free libraries. In this
|
||||||
|
case, there is little to gain by limiting the free library to free
|
||||||
|
software only, so we use the Lesser General Public License.
|
||||||
|
|
||||||
|
In other cases, permission to use a particular library in non-free
|
||||||
|
programs enables a greater number of people to use a large body of
|
||||||
|
free software. For example, permission to use the GNU C Library in
|
||||||
|
non-free programs enables many more people to use the whole GNU
|
||||||
|
operating system, as well as its variant, the GNU/Linux operating
|
||||||
|
system.
|
||||||
|
|
||||||
|
Although the Lesser General Public License is Less protective of the
|
||||||
|
users' freedom, it does ensure that the user of a program that is
|
||||||
|
linked with the Library has the freedom and the wherewithal to run
|
||||||
|
that program using a modified version of the Library.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow. Pay close attention to the difference between a
|
||||||
|
"work based on the library" and a "work that uses the library". The
|
||||||
|
former contains code derived from the library, whereas the latter must
|
||||||
|
be combined with the library in order to run.
|
||||||
|
|
||||||
|
GNU LESSER GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License Agreement applies to any software library or other
|
||||||
|
program which contains a notice placed by the copyright holder or
|
||||||
|
other authorized party saying it may be distributed under the terms of
|
||||||
|
this Lesser General Public License (also called "this License").
|
||||||
|
Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
A "library" means a collection of software functions and/or data
|
||||||
|
prepared so as to be conveniently linked with application programs
|
||||||
|
(which use some of those functions and data) to form executables.
|
||||||
|
|
||||||
|
The "Library", below, refers to any such software library or work
|
||||||
|
which has been distributed under these terms. A "work based on the
|
||||||
|
Library" means either the Library or any derivative work under
|
||||||
|
copyright law: that is to say, a work containing the Library or a
|
||||||
|
portion of it, either verbatim or with modifications and/or translated
|
||||||
|
straightforwardly into another language. (Hereinafter, translation is
|
||||||
|
included without limitation in the term "modification".)
|
||||||
|
|
||||||
|
"Source code" for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For a library, complete source code means
|
||||||
|
all the source code for all modules it contains, plus any associated
|
||||||
|
interface definition files, plus the scripts used to control compilation
|
||||||
|
and installation of the library.
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running a program using the Library is not restricted, and output from
|
||||||
|
such a program is covered only if its contents constitute a work based
|
||||||
|
on the Library (independent of the use of the Library in a tool for
|
||||||
|
writing it). Whether that is true depends on what the Library does
|
||||||
|
and what the program that uses the Library does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Library's
|
||||||
|
complete source code as you receive it, in any medium, provided that
|
||||||
|
you conspicuously and appropriately publish on each copy an
|
||||||
|
appropriate copyright notice and disclaimer of warranty; keep intact
|
||||||
|
all the notices that refer to this License and to the absence of any
|
||||||
|
warranty; and distribute a copy of this License along with the
|
||||||
|
Library.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy,
|
||||||
|
and you may at your option offer warranty protection in exchange for a
|
||||||
|
fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Library or any portion
|
||||||
|
of it, thus forming a work based on the Library, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The modified work must itself be a software library.
|
||||||
|
|
||||||
|
b) You must cause the files modified to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
c) You must cause the whole of the work to be licensed at no
|
||||||
|
charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
d) If a facility in the modified Library refers to a function or a
|
||||||
|
table of data to be supplied by an application program that uses
|
||||||
|
the facility, other than as an argument passed when the facility
|
||||||
|
is invoked, then you must make a good faith effort to ensure that,
|
||||||
|
in the event an application does not supply such function or
|
||||||
|
table, the facility still operates, and performs whatever part of
|
||||||
|
its purpose remains meaningful.
|
||||||
|
|
||||||
|
(For example, a function in a library to compute square roots has
|
||||||
|
a purpose that is entirely well-defined independent of the
|
||||||
|
application. Therefore, Subsection 2d requires that any
|
||||||
|
application-supplied function or table used by this function must
|
||||||
|
be optional: if the application does not supply it, the square
|
||||||
|
root function must still compute square roots.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Library,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Library, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote
|
||||||
|
it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Library.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Library
|
||||||
|
with the Library (or with a work based on the Library) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may opt to apply the terms of the ordinary GNU General Public
|
||||||
|
License instead of this License to a given copy of the Library. To do
|
||||||
|
this, you must alter all the notices that refer to this License, so
|
||||||
|
that they refer to the ordinary GNU General Public License, version 2,
|
||||||
|
instead of to this License. (If a newer version than version 2 of the
|
||||||
|
ordinary GNU General Public License has appeared, then you can specify
|
||||||
|
that version instead if you wish.) Do not make any other change in
|
||||||
|
these notices.
|
||||||
|
|
||||||
|
Once this change is made in a given copy, it is irreversible for
|
||||||
|
that copy, so the ordinary GNU General Public License applies to all
|
||||||
|
subsequent copies and derivative works made from that copy.
|
||||||
|
|
||||||
|
This option is useful when you wish to copy part of the code of
|
||||||
|
the Library into a program that is not a library.
|
||||||
|
|
||||||
|
4. You may copy and distribute the Library (or a portion or
|
||||||
|
derivative of it, under Section 2) in object code or executable form
|
||||||
|
under the terms of Sections 1 and 2 above provided that you accompany
|
||||||
|
it with the complete corresponding machine-readable source code, which
|
||||||
|
must be distributed under the terms of Sections 1 and 2 above on a
|
||||||
|
medium customarily used for software interchange.
|
||||||
|
|
||||||
|
If distribution of object code is made by offering access to copy
|
||||||
|
from a designated place, then offering equivalent access to copy the
|
||||||
|
source code from the same place satisfies the requirement to
|
||||||
|
distribute the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
5. A program that contains no derivative of any portion of the
|
||||||
|
Library, but is designed to work with the Library by being compiled or
|
||||||
|
linked with it, is called a "work that uses the Library". Such a
|
||||||
|
work, in isolation, is not a derivative work of the Library, and
|
||||||
|
therefore falls outside the scope of this License.
|
||||||
|
|
||||||
|
However, linking a "work that uses the Library" with the Library
|
||||||
|
creates an executable that is a derivative of the Library (because it
|
||||||
|
contains portions of the Library), rather than a "work that uses the
|
||||||
|
library". The executable is therefore covered by this License.
|
||||||
|
Section 6 states terms for distribution of such executables.
|
||||||
|
|
||||||
|
When a "work that uses the Library" uses material from a header file
|
||||||
|
that is part of the Library, the object code for the work may be a
|
||||||
|
derivative work of the Library even though the source code is not.
|
||||||
|
Whether this is true is especially significant if the work can be
|
||||||
|
linked without the Library, or if the work is itself a library. The
|
||||||
|
threshold for this to be true is not precisely defined by law.
|
||||||
|
|
||||||
|
If such an object file uses only numerical parameters, data
|
||||||
|
structure layouts and accessors, and small macros and small inline
|
||||||
|
functions (ten lines or less in length), then the use of the object
|
||||||
|
file is unrestricted, regardless of whether it is legally a derivative
|
||||||
|
work. (Executables containing this object code plus portions of the
|
||||||
|
Library will still fall under Section 6.)
|
||||||
|
|
||||||
|
Otherwise, if the work is a derivative of the Library, you may
|
||||||
|
distribute the object code for the work under the terms of Section 6.
|
||||||
|
Any executables containing that work also fall under Section 6,
|
||||||
|
whether or not they are linked directly with the Library itself.
|
||||||
|
|
||||||
|
6. As an exception to the Sections above, you may also combine or
|
||||||
|
link a "work that uses the Library" with the Library to produce a
|
||||||
|
work containing portions of the Library, and distribute that work
|
||||||
|
under terms of your choice, provided that the terms permit
|
||||||
|
modification of the work for the customer's own use and reverse
|
||||||
|
engineering for debugging such modifications.
|
||||||
|
|
||||||
|
You must give prominent notice with each copy of the work that the
|
||||||
|
Library is used in it and that the Library and its use are covered by
|
||||||
|
this License. You must supply a copy of this License. If the work
|
||||||
|
during execution displays copyright notices, you must include the
|
||||||
|
copyright notice for the Library among them, as well as a reference
|
||||||
|
directing the user to the copy of this License. Also, you must do one
|
||||||
|
of these things:
|
||||||
|
|
||||||
|
a) Accompany the work with the complete corresponding
|
||||||
|
machine-readable source code for the Library including whatever
|
||||||
|
changes were used in the work (which must be distributed under
|
||||||
|
Sections 1 and 2 above); and, if the work is an executable linked
|
||||||
|
with the Library, with the complete machine-readable "work that
|
||||||
|
uses the Library", as object code and/or source code, so that the
|
||||||
|
user can modify the Library and then relink to produce a modified
|
||||||
|
executable containing the modified Library. (It is understood
|
||||||
|
that the user who changes the contents of definitions files in the
|
||||||
|
Library will not necessarily be able to recompile the application
|
||||||
|
to use the modified definitions.)
|
||||||
|
|
||||||
|
b) Use a suitable shared library mechanism for linking with the
|
||||||
|
Library. A suitable mechanism is one that (1) uses at run time a
|
||||||
|
copy of the library already present on the user's computer system,
|
||||||
|
rather than copying library functions into the executable, and (2)
|
||||||
|
will operate properly with a modified version of the library, if
|
||||||
|
the user installs one, as long as the modified version is
|
||||||
|
interface-compatible with the version that the work was made with.
|
||||||
|
|
||||||
|
c) Accompany the work with a written offer, valid for at
|
||||||
|
least three years, to give the same user the materials
|
||||||
|
specified in Subsection 6a, above, for a charge no more
|
||||||
|
than the cost of performing this distribution.
|
||||||
|
|
||||||
|
d) If distribution of the work is made by offering access to copy
|
||||||
|
from a designated place, offer equivalent access to copy the above
|
||||||
|
specified materials from the same place.
|
||||||
|
|
||||||
|
e) Verify that the user has already received a copy of these
|
||||||
|
materials or that you have already sent this user a copy.
|
||||||
|
|
||||||
|
For an executable, the required form of the "work that uses the
|
||||||
|
Library" must include any data and utility programs needed for
|
||||||
|
reproducing the executable from it. However, as a special exception,
|
||||||
|
the materials to be distributed need not include anything that is
|
||||||
|
normally distributed (in either source or binary form) with the major
|
||||||
|
components (compiler, kernel, and so on) of the operating system on
|
||||||
|
which the executable runs, unless that component itself accompanies
|
||||||
|
the executable.
|
||||||
|
|
||||||
|
It may happen that this requirement contradicts the license
|
||||||
|
restrictions of other proprietary libraries that do not normally
|
||||||
|
accompany the operating system. Such a contradiction means you cannot
|
||||||
|
use both them and the Library together in an executable that you
|
||||||
|
distribute.
|
||||||
|
|
||||||
|
7. You may place library facilities that are a work based on the
|
||||||
|
Library side-by-side in a single library together with other library
|
||||||
|
facilities not covered by this License, and distribute such a combined
|
||||||
|
library, provided that the separate distribution of the work based on
|
||||||
|
the Library and of the other library facilities is otherwise
|
||||||
|
permitted, and provided that you do these two things:
|
||||||
|
|
||||||
|
a) Accompany the combined library with a copy of the same work
|
||||||
|
based on the Library, uncombined with any other library
|
||||||
|
facilities. This must be distributed under the terms of the
|
||||||
|
Sections above.
|
||||||
|
|
||||||
|
b) Give prominent notice with the combined library of the fact
|
||||||
|
that part of it is a work based on the Library, and explaining
|
||||||
|
where to find the accompanying uncombined form of the same work.
|
||||||
|
|
||||||
|
8. You may not copy, modify, sublicense, link with, or distribute
|
||||||
|
the Library except as expressly provided under this License. Any
|
||||||
|
attempt otherwise to copy, modify, sublicense, link with, or
|
||||||
|
distribute the Library is void, and will automatically terminate your
|
||||||
|
rights under this License. However, parties who have received copies,
|
||||||
|
or rights, from you under this License will not have their licenses
|
||||||
|
terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
9. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Library or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Library (or any work based on the
|
||||||
|
Library), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Library or works based on it.
|
||||||
|
|
||||||
|
10. Each time you redistribute the Library (or any work based on the
|
||||||
|
Library), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute, link with or modify the Library
|
||||||
|
subject to these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties with
|
||||||
|
this License.
|
||||||
|
|
||||||
|
11. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Library at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Library by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Library.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any
|
||||||
|
particular circumstance, the balance of the section is intended to apply,
|
||||||
|
and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
12. If the distribution and/or use of the Library is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Library under this License may add
|
||||||
|
an explicit geographical distribution limitation excluding those countries,
|
||||||
|
so that distribution is permitted only in or among countries not thus
|
||||||
|
excluded. In such case, this License incorporates the limitation as if
|
||||||
|
written in the body of this License.
|
||||||
|
|
||||||
|
13. The Free Software Foundation may publish revised and/or new
|
||||||
|
versions of the Lesser General Public License from time to time.
|
||||||
|
Such new versions will be similar in spirit to the present version,
|
||||||
|
but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Library
|
||||||
|
specifies a version number of this License which applies to it and
|
||||||
|
"any later version", you have the option of following the terms and
|
||||||
|
conditions either of that version or of any later version published by
|
||||||
|
the Free Software Foundation. If the Library does not specify a
|
||||||
|
license version number, you may choose any version ever published by
|
||||||
|
the Free Software Foundation.
|
||||||
|
|
||||||
|
14. If you wish to incorporate parts of the Library into other free
|
||||||
|
programs whose distribution conditions are incompatible with these,
|
||||||
|
write to the author to ask for permission. For software which is
|
||||||
|
copyrighted by the Free Software Foundation, write to the Free
|
||||||
|
Software Foundation; we sometimes make exceptions for this. Our
|
||||||
|
decision will be guided by the two goals of preserving the free status
|
||||||
|
of all derivatives of our free software and of promoting the sharing
|
||||||
|
and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
|
||||||
|
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
|
||||||
|
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
|
||||||
|
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
|
||||||
|
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
|
||||||
|
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
|
||||||
|
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
|
||||||
|
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
|
||||||
|
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
|
||||||
|
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
|
||||||
|
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
|
||||||
|
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
|
||||||
|
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
|
||||||
|
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
|
||||||
|
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
28
distribution/osx/Launcher/README.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
Code reused from duplicati, licensed under LGPL 2.1
|
||||||
|
Modified for Sonarr by Taloth Saldono
|
||||||
|
|
||||||
|
see here for the original source: https://github.com/duplicati/duplicati/tree/679981d29f8a6e445d3c1e6d41e72a673ffaa653/Installer/OSX
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
Sonarr as a whole is licensed under GPL 3.0 as specified in the git repository root.
|
||||||
|
|
||||||
|
But to preserve the original intent of the duplicati project, the modified versions of the sources in this folder are dual licensed under LGPL 2.1 and GPL 3.0.
|
||||||
|
Note: This exception can be freely removed in any copy of Sonarr sources as per LGPL/GPL licensing terms.
|
||||||
|
|
||||||
|
A copy of the LGPL 2.1 license is included in the LICENSE.LGPL.md file.
|
||||||
|
|
||||||
|
Purpose
|
||||||
|
-------
|
||||||
|
|
||||||
|
The Launcher is a bootstrap/shim application that checks if the appropriate version of mono is installed and subsequently use it to execute Sonarr.
|
||||||
|
By using a separate application, instead of a shell script, this allows the user to assign certain operating system permissions to Sonarr specifically.
|
||||||
|
|
||||||
|
Compiling the Launcher
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
You need an OSX installation with xcode
|
||||||
|
Then run compile.sh in a terminal
|
||||||
|
|
||||||
|
The generated dist/Launcher can be renamed to Sonarr and Sonarr.Update to serve as shims to run Sonarr.exe and Sonarr.Update.exe respectively.
|
||||||
BIN
distribution/osx/Launcher/dist/Launcher
vendored
Executable file
32
distribution/osx/Launcher/src/Launcher.m
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#import "run-with-mono.h"
|
||||||
|
#import "PFMoveApplication.h"
|
||||||
|
|
||||||
|
int const MONO_VERSION_MAJOR = 5;
|
||||||
|
int const MONO_VERSION_MINOR = 20;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
@autoreleasepool {
|
||||||
|
// Use our own executable name so the same compiled binary to be used for forks
|
||||||
|
NSString * const FileName = NSProcessInfo.processInfo.arguments[0].lastPathComponent;
|
||||||
|
|
||||||
|
// Sonarr.Update.exe
|
||||||
|
NSString * const ASSEMBLY = [NSString stringWithFormat:@"%@.exe", FileName];
|
||||||
|
|
||||||
|
// Sonarr Update
|
||||||
|
NSString * const APP_NAME = [FileName stringByReplacingOccurrencesOfString:@"." withString:@" "];
|
||||||
|
|
||||||
|
// -sonarrupdate
|
||||||
|
NSString * const PROCESS_NAME = [NSString stringWithFormat:@"-%@", [FileName stringByReplacingOccurrencesOfString:@"." withString:@""].lowercaseString];
|
||||||
|
|
||||||
|
@try
|
||||||
|
{
|
||||||
|
PFMoveToApplicationsFolderIfNecessary();
|
||||||
|
}
|
||||||
|
@catch (NSException * ex)
|
||||||
|
{
|
||||||
|
NSLog(@"Translocation/Quarantine check failed, starting normally. Reason: %@", ex.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [RunWithMono runAssemblyWithMono:APP_NAME procnamesuffix:PROCESS_NAME assembly:ASSEMBLY major:MONO_VERSION_MAJOR minor:MONO_VERSION_MINOR];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
distribution/osx/Launcher/src/PFMoveApplication.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// PFMoveApplication.h, version 1.24
|
||||||
|
// LetsMove
|
||||||
|
//
|
||||||
|
// Created by Andy Kim at Potion Factory LLC on 9/17/09
|
||||||
|
//
|
||||||
|
// The contents of this file are dedicated to the public domain.
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
/**
|
||||||
|
Moves the running application to ~/Applications or /Applications if the former does not exist.
|
||||||
|
After the move, it relaunches app from the new location.
|
||||||
|
DOES NOT work for sandboxed applications.
|
||||||
|
|
||||||
|
Call from \c NSApplication's delegate method \c -applicationWillFinishLaunching: method. */
|
||||||
|
void PFMoveToApplicationsFolderIfNecessary(void);
|
||||||
|
|
||||||
|
/**
|
||||||
|
Check whether an app move is currently in progress.
|
||||||
|
Returns YES if LetsMove is currently in-progress trying to move the app to the Applications folder, or NO otherwise.
|
||||||
|
This can be used to work around a crash with apps that terminate after last window is closed.
|
||||||
|
See https://github.com/potionfactory/LetsMove/issues/64 for details. */
|
||||||
|
BOOL PFMoveIsInProgress(void);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
565
distribution/osx/Launcher/src/PFMoveApplication.m
Normal file
@@ -0,0 +1,565 @@
|
|||||||
|
//
|
||||||
|
// PFMoveApplication.m, version 1.24
|
||||||
|
// LetsMove
|
||||||
|
//
|
||||||
|
// Created by Andy Kim at Potion Factory LLC on 9/17/09
|
||||||
|
//
|
||||||
|
// The contents of this file are dedicated to the public domain.
|
||||||
|
|
||||||
|
#import "PFMoveApplication.h"
|
||||||
|
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import <Security/Security.h>
|
||||||
|
#import <dlfcn.h>
|
||||||
|
#import <sys/mount.h>
|
||||||
|
|
||||||
|
@interface LetsMove : NSObject
|
||||||
|
@end
|
||||||
|
|
||||||
|
@implementation LetsMove
|
||||||
|
+ (NSBundle *)bundle {
|
||||||
|
return [NSBundle bundleForClass:self];
|
||||||
|
}
|
||||||
|
@end
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
// These are macros to be able to use custom i18n tools
|
||||||
|
#define _I10NS(nsstr) NSLocalizedStringFromTableInBundle(nsstr, @"MoveApplication", [LetsMove bundle], nil)
|
||||||
|
#define kStrMoveApplicationCouldNotMove _I10NS(@"Could not move to Applications folder")
|
||||||
|
#define kStrMoveApplicationQuestionTitle _I10NS(@"Move to Applications folder?")
|
||||||
|
#define kStrMoveApplicationQuestionTitleHome _I10NS(@"Move to Applications folder in your Home folder?")
|
||||||
|
#define kStrMoveApplicationQuestionMessage _I10NS(@"I can move myself to the Applications folder if you'd like.")
|
||||||
|
#define kStrMoveApplicationButtonMove _I10NS(@"Move to Applications Folder")
|
||||||
|
#define kStrMoveApplicationButtonDoNotMove _I10NS(@"Do Not Move")
|
||||||
|
#define kStrMoveApplicationQuestionInfoWillRequirePasswd _I10NS(@"Note that this will require an administrator password.")
|
||||||
|
#define kStrMoveApplicationQuestionInfoInDownloadsFolder _I10NS(@"This will keep your Downloads folder uncluttered.")
|
||||||
|
|
||||||
|
// Needs to be defined for compiling under 10.5 SDK
|
||||||
|
#ifndef NSAppKitVersionNumber10_5
|
||||||
|
#define NSAppKitVersionNumber10_5 949
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// By default, we use a small control/font for the suppression button.
|
||||||
|
// If you prefer to use the system default (to match your other alerts),
|
||||||
|
// set this to 0.
|
||||||
|
#define PFUseSmallAlertSuppressCheckbox 1
|
||||||
|
|
||||||
|
|
||||||
|
static NSString *AlertSuppressKey = @"moveToApplicationsFolderAlertSuppress";
|
||||||
|
static BOOL MoveInProgress = NO;
|
||||||
|
|
||||||
|
// Helper functions
|
||||||
|
static NSString *PreferredInstallLocation(BOOL *isUserDirectory);
|
||||||
|
static BOOL IsInApplicationsFolder(NSString *path);
|
||||||
|
static BOOL IsInDownloadsFolder(NSString *path);
|
||||||
|
static BOOL IsApplicationAtPathRunning(NSString *path);
|
||||||
|
static BOOL IsApplicationAtPathNested(NSString *path);
|
||||||
|
static NSString *ContainingDiskImageDevice(NSString *path);
|
||||||
|
static BOOL Trash(NSString *path);
|
||||||
|
static BOOL DeleteOrTrash(NSString *path);
|
||||||
|
static BOOL AuthorizedInstall(NSString *srcPath, NSString *dstPath, BOOL *canceled);
|
||||||
|
static BOOL CopyBundle(NSString *srcPath, NSString *dstPath);
|
||||||
|
static NSString *ShellQuotedString(NSString *string);
|
||||||
|
static void Relaunch(NSString *destinationPath);
|
||||||
|
|
||||||
|
// Main worker function
|
||||||
|
void PFMoveToApplicationsFolderIfNecessary(void) {
|
||||||
|
|
||||||
|
// Make sure to do our work on the main thread.
|
||||||
|
// Apparently Electron apps need this for things to work properly.
|
||||||
|
if (![NSThread isMainThread]) {
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
|
PFMoveToApplicationsFolderIfNecessary();
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if user suppressed the alert before
|
||||||
|
if ([[NSUserDefaults standardUserDefaults] boolForKey:AlertSuppressKey]) return;
|
||||||
|
|
||||||
|
// Path of the bundle
|
||||||
|
NSString *bundlePath = [[NSBundle mainBundle] bundlePath];
|
||||||
|
|
||||||
|
// Check if the bundle is embedded in another application
|
||||||
|
BOOL isNestedApplication = IsApplicationAtPathNested(bundlePath);
|
||||||
|
|
||||||
|
// Skip if the application is already in some Applications folder,
|
||||||
|
// unless it's inside another app's bundle.
|
||||||
|
if (IsInApplicationsFolder(bundlePath) && !isNestedApplication) return;
|
||||||
|
|
||||||
|
// OK, looks like we'll need to do a move - set the status variable appropriately
|
||||||
|
MoveInProgress = YES;
|
||||||
|
|
||||||
|
// File Manager
|
||||||
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
|
|
||||||
|
// Are we on a disk image?
|
||||||
|
NSString *diskImageDevice = ContainingDiskImageDevice(bundlePath);
|
||||||
|
|
||||||
|
// Since we are good to go, get the preferred installation directory.
|
||||||
|
BOOL installToUserApplications = NO;
|
||||||
|
NSString *applicationsDirectory = PreferredInstallLocation(&installToUserApplications);
|
||||||
|
NSString *bundleName = [bundlePath lastPathComponent];
|
||||||
|
NSString *destinationPath = [applicationsDirectory stringByAppendingPathComponent:bundleName];
|
||||||
|
|
||||||
|
// Check if we need admin password to write to the Applications directory
|
||||||
|
BOOL needAuthorization = ([fm isWritableFileAtPath:applicationsDirectory] == NO);
|
||||||
|
|
||||||
|
// Check if the destination bundle is already there but not writable
|
||||||
|
needAuthorization |= ([fm fileExistsAtPath:destinationPath] && ![fm isWritableFileAtPath:destinationPath]);
|
||||||
|
|
||||||
|
// Setup the alert
|
||||||
|
NSAlert *alert = [[[NSAlert alloc] init] autorelease];
|
||||||
|
{
|
||||||
|
NSString *informativeText = nil;
|
||||||
|
|
||||||
|
[alert setMessageText:(installToUserApplications ? kStrMoveApplicationQuestionTitleHome : kStrMoveApplicationQuestionTitle)];
|
||||||
|
|
||||||
|
informativeText = kStrMoveApplicationQuestionMessage;
|
||||||
|
|
||||||
|
if (needAuthorization) {
|
||||||
|
informativeText = [informativeText stringByAppendingString:@" "];
|
||||||
|
informativeText = [informativeText stringByAppendingString:kStrMoveApplicationQuestionInfoWillRequirePasswd];
|
||||||
|
}
|
||||||
|
else if (IsInDownloadsFolder(bundlePath)) {
|
||||||
|
// Don't mention this stuff if we need authentication. The informative text is long enough as it is in that case.
|
||||||
|
informativeText = [informativeText stringByAppendingString:@" "];
|
||||||
|
informativeText = [informativeText stringByAppendingString:kStrMoveApplicationQuestionInfoInDownloadsFolder];
|
||||||
|
}
|
||||||
|
|
||||||
|
[alert setInformativeText:informativeText];
|
||||||
|
|
||||||
|
// Add accept button
|
||||||
|
[alert addButtonWithTitle:kStrMoveApplicationButtonMove];
|
||||||
|
|
||||||
|
// Add deny button
|
||||||
|
NSButton *cancelButton = [alert addButtonWithTitle:kStrMoveApplicationButtonDoNotMove];
|
||||||
|
[cancelButton setKeyEquivalent:[NSString stringWithFormat:@"%C", 0x1b]]; // Escape key
|
||||||
|
|
||||||
|
// Setup suppression button
|
||||||
|
[alert setShowsSuppressionButton:YES];
|
||||||
|
|
||||||
|
if (PFUseSmallAlertSuppressCheckbox) {
|
||||||
|
NSCell *cell = [[alert suppressionButton] cell];
|
||||||
|
[cell setControlSize:NSSmallControlSize];
|
||||||
|
[cell setFont:[NSFont systemFontOfSize:[NSFont smallSystemFontSize]]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activate app -- work-around for focus issues related to "scary file from internet" OS dialog.
|
||||||
|
if (![NSApp isActive]) {
|
||||||
|
[NSApp activateIgnoringOtherApps:YES];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([alert runModal] == NSAlertFirstButtonReturn) {
|
||||||
|
NSLog(@"INFO -- Moving myself to the Applications folder");
|
||||||
|
|
||||||
|
// Move
|
||||||
|
if (needAuthorization) {
|
||||||
|
BOOL authorizationCanceled;
|
||||||
|
|
||||||
|
if (!AuthorizedInstall(bundlePath, destinationPath, &authorizationCanceled)) {
|
||||||
|
if (authorizationCanceled) {
|
||||||
|
NSLog(@"INFO -- Not moving because user canceled authorization");
|
||||||
|
MoveInProgress = NO;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
NSLog(@"ERROR -- Could not copy myself to /Applications with authorization");
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If a copy already exists in the Applications folder, put it in the Trash
|
||||||
|
if ([fm fileExistsAtPath:destinationPath]) {
|
||||||
|
// But first, make sure that it's not running
|
||||||
|
if (IsApplicationAtPathRunning(destinationPath)) {
|
||||||
|
// Give the running app focus and terminate myself
|
||||||
|
NSLog(@"INFO -- Switching to an already running version");
|
||||||
|
[[NSTask launchedTaskWithLaunchPath:@"/usr/bin/open" arguments:[NSArray arrayWithObject:destinationPath]] waitUntilExit];
|
||||||
|
MoveInProgress = NO;
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (!Trash([applicationsDirectory stringByAppendingPathComponent:bundleName]))
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CopyBundle(bundlePath, destinationPath)) {
|
||||||
|
NSLog(@"ERROR -- Could not copy myself to %@", destinationPath);
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trash the original app. It's okay if this fails.
|
||||||
|
// NOTE: This final delete does not work if the source bundle is in a network mounted volume.
|
||||||
|
// Calling rm or file manager's delete method doesn't work either. It's unlikely to happen
|
||||||
|
// but it'd be great if someone could fix this.
|
||||||
|
if (!isNestedApplication && diskImageDevice == nil && !DeleteOrTrash(bundlePath)) {
|
||||||
|
NSLog(@"WARNING -- Could not delete application after moving it to Applications folder");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relaunch.
|
||||||
|
Relaunch(destinationPath);
|
||||||
|
|
||||||
|
// Launched from within a disk image? -- unmount (if no files are open after 5 seconds,
|
||||||
|
// otherwise leave it mounted).
|
||||||
|
if (diskImageDevice && !isNestedApplication) {
|
||||||
|
NSString *script = [NSString stringWithFormat:@"(/bin/sleep 5 && /usr/bin/hdiutil detach %@) &", ShellQuotedString(diskImageDevice)];
|
||||||
|
[NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]];
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveInProgress = NO;
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
// Save the alert suppress preference if checked
|
||||||
|
else if ([[alert suppressionButton] state] == NSOnState) {
|
||||||
|
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:AlertSuppressKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveInProgress = NO;
|
||||||
|
return;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
{
|
||||||
|
// Show failure message
|
||||||
|
alert = [[[NSAlert alloc] init] autorelease];
|
||||||
|
[alert setMessageText:kStrMoveApplicationCouldNotMove];
|
||||||
|
[alert runModal];
|
||||||
|
MoveInProgress = NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL PFMoveIsInProgress() {
|
||||||
|
return MoveInProgress;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma mark -
|
||||||
|
#pragma mark Helper Functions
|
||||||
|
|
||||||
|
static NSString *PreferredInstallLocation(BOOL *isUserDirectory) {
|
||||||
|
// Return the preferred install location.
|
||||||
|
// Assume that if the user has a ~/Applications folder, they'd prefer their
|
||||||
|
// applications to go there.
|
||||||
|
|
||||||
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
|
|
||||||
|
/*
|
||||||
|
NSArray *userApplicationsDirs = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSUserDomainMask, YES);
|
||||||
|
|
||||||
|
if ([userApplicationsDirs count] > 0) {
|
||||||
|
NSString *userApplicationsDir = [userApplicationsDirs objectAtIndex:0];
|
||||||
|
BOOL isDirectory;
|
||||||
|
|
||||||
|
if ([fm fileExistsAtPath:userApplicationsDir isDirectory:&isDirectory] && isDirectory) {
|
||||||
|
// User Applications directory exists. Get the directory contents.
|
||||||
|
NSArray *contents = [fm contentsOfDirectoryAtPath:userApplicationsDir error:NULL];
|
||||||
|
|
||||||
|
// Check if there is at least one ".app" inside the directory.
|
||||||
|
for (NSString *contentsPath in contents) {
|
||||||
|
if ([[contentsPath pathExtension] isEqualToString:@"app"]) {
|
||||||
|
if (isUserDirectory) *isUserDirectory = YES;
|
||||||
|
return [userApplicationsDir stringByResolvingSymlinksInPath];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// No user Applications directory in use. Return the machine local Applications directory
|
||||||
|
if (isUserDirectory) *isUserDirectory = NO;
|
||||||
|
|
||||||
|
return [[NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSLocalDomainMask, YES) lastObject] stringByResolvingSymlinksInPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL IsInApplicationsFolder(NSString *path) {
|
||||||
|
// Check all the normal Application directories
|
||||||
|
NSArray *applicationDirs = NSSearchPathForDirectoriesInDomains(NSApplicationDirectory, NSAllDomainsMask, YES);
|
||||||
|
for (NSString *appDir in applicationDirs) {
|
||||||
|
if ([path hasPrefix:appDir]) return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also, handle the case that the user has some other Application directory (perhaps on a separate data partition).
|
||||||
|
if ([[path pathComponents] containsObject:@"Applications"]) return YES;
|
||||||
|
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL IsInDownloadsFolder(NSString *path) {
|
||||||
|
NSArray *downloadDirs = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSAllDomainsMask, YES);
|
||||||
|
for (NSString *downloadsDirPath in downloadDirs) {
|
||||||
|
if ([path hasPrefix:downloadsDirPath]) return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL IsApplicationAtPathRunning(NSString *bundlePath) {
|
||||||
|
bundlePath = [bundlePath stringByStandardizingPath];
|
||||||
|
|
||||||
|
#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
|
||||||
|
// Use the new API on 10.6 or higher to determine if the app is already running
|
||||||
|
if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) {
|
||||||
|
for (NSRunningApplication *runningApplication in [[NSWorkspace sharedWorkspace] runningApplications]) {
|
||||||
|
NSString *runningAppBundlePath = [[[runningApplication bundleURL] path] stringByStandardizingPath];
|
||||||
|
if ([runningAppBundlePath isEqualToString:bundlePath]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
// Use the shell to determine if the app is already running on systems 10.5 or lower
|
||||||
|
NSString *script = [NSString stringWithFormat:@"/bin/ps ax -o comm | /usr/bin/grep %@/ | /usr/bin/grep -v grep >/dev/null", ShellQuotedString(bundlePath)];
|
||||||
|
NSTask *task = [NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]];
|
||||||
|
[task waitUntilExit];
|
||||||
|
|
||||||
|
// If the task terminated with status 0, it means that the final grep produced 1 or more lines of output.
|
||||||
|
// Which means that the app is already running
|
||||||
|
return [task terminationStatus] == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL IsApplicationAtPathNested(NSString *path) {
|
||||||
|
NSString *containingPath = [path stringByDeletingLastPathComponent];
|
||||||
|
|
||||||
|
NSArray *components = [containingPath pathComponents];
|
||||||
|
for (NSString *component in components) {
|
||||||
|
if ([[component pathExtension] isEqualToString:@"app"]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSString *ContainingDiskImageDevice(NSString *path) {
|
||||||
|
NSString *containingPath = [path stringByDeletingLastPathComponent];
|
||||||
|
|
||||||
|
struct statfs fs;
|
||||||
|
if (statfs([containingPath fileSystemRepresentation], &fs) || (fs.f_flags & MNT_ROOTFS))
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
NSString *device = [[NSFileManager defaultManager] stringWithFileSystemRepresentation:fs.f_mntfromname length:strlen(fs.f_mntfromname)];
|
||||||
|
|
||||||
|
NSTask *hdiutil = [[[NSTask alloc] init] autorelease];
|
||||||
|
[hdiutil setLaunchPath:@"/usr/bin/hdiutil"];
|
||||||
|
[hdiutil setArguments:[NSArray arrayWithObjects:@"info", @"-plist", nil]];
|
||||||
|
[hdiutil setStandardOutput:[NSPipe pipe]];
|
||||||
|
[hdiutil launch];
|
||||||
|
[hdiutil waitUntilExit];
|
||||||
|
|
||||||
|
NSData *data = [[[hdiutil standardOutput] fileHandleForReading] readDataToEndOfFile];
|
||||||
|
NSDictionary *info = nil;
|
||||||
|
#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
|
||||||
|
if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) {
|
||||||
|
info = [NSPropertyListSerialization propertyListWithData:data options:NSPropertyListImmutable format:NULL error:NULL];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
#endif
|
||||||
|
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10
|
||||||
|
info = [NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:NULL];
|
||||||
|
#endif
|
||||||
|
#if MAC_OS_X_VERSION_MAX_ALLOWED > MAC_OS_X_VERSION_10_5
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (![info isKindOfClass:[NSDictionary class]]) return nil;
|
||||||
|
|
||||||
|
NSArray *images = (NSArray *)[info objectForKey:@"images"];
|
||||||
|
if (![images isKindOfClass:[NSArray class]]) return nil;
|
||||||
|
|
||||||
|
for (NSDictionary *image in images) {
|
||||||
|
if (![image isKindOfClass:[NSDictionary class]]) return nil;
|
||||||
|
|
||||||
|
id systemEntities = [image objectForKey:@"system-entities"];
|
||||||
|
if (![systemEntities isKindOfClass:[NSArray class]]) return nil;
|
||||||
|
|
||||||
|
for (NSDictionary *systemEntity in systemEntities) {
|
||||||
|
if (![systemEntity isKindOfClass:[NSDictionary class]]) return nil;
|
||||||
|
|
||||||
|
NSString *devEntry = [systemEntity objectForKey:@"dev-entry"];
|
||||||
|
if (![devEntry isKindOfClass:[NSString class]]) return nil;
|
||||||
|
|
||||||
|
if ([devEntry isEqualToString:device])
|
||||||
|
return device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL Trash(NSString *path) {
|
||||||
|
BOOL result = NO;
|
||||||
|
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_8
|
||||||
|
if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_8) {
|
||||||
|
result = [[NSFileManager defaultManager] trashItemAtURL:[NSURL fileURLWithPath:path] resultingItemURL:NULL error:NULL];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_11
|
||||||
|
if (!result) {
|
||||||
|
result = [[NSWorkspace sharedWorkspace] performFileOperation:NSWorkspaceRecycleOperation
|
||||||
|
source:[path stringByDeletingLastPathComponent]
|
||||||
|
destination:@""
|
||||||
|
files:[NSArray arrayWithObject:[path lastPathComponent]]
|
||||||
|
tag:NULL];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// As a last resort try trashing with AppleScript.
|
||||||
|
// This allows us to trash the app in macOS Sierra even when the app is running inside
|
||||||
|
// an app translocation image.
|
||||||
|
if (!result) {
|
||||||
|
NSAppleScript *appleScript = [[[NSAppleScript alloc] initWithSource:
|
||||||
|
[NSString stringWithFormat:@"\
|
||||||
|
set theFile to POSIX file \"%@\" \n\
|
||||||
|
tell application \"Finder\" \n\
|
||||||
|
move theFile to trash \n\
|
||||||
|
end tell", path]] autorelease];
|
||||||
|
NSDictionary *errorDict = nil;
|
||||||
|
NSAppleEventDescriptor *scriptResult = [appleScript executeAndReturnError:&errorDict];
|
||||||
|
if (scriptResult == nil) {
|
||||||
|
NSLog(@"Trash AppleScript error: %@", errorDict);
|
||||||
|
}
|
||||||
|
result = (scriptResult != nil);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result) {
|
||||||
|
NSLog(@"ERROR -- Could not trash '%@'", path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL DeleteOrTrash(NSString *path) {
|
||||||
|
NSError *error;
|
||||||
|
|
||||||
|
if ([[NSFileManager defaultManager] removeItemAtPath:path error:&error]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Don't log warning if on Sierra and running inside App Translocation path
|
||||||
|
if ([path rangeOfString:@"/AppTranslocation/"].location == NSNotFound)
|
||||||
|
NSLog(@"WARNING -- Could not delete '%@': %@", path, [error localizedDescription]);
|
||||||
|
|
||||||
|
return Trash(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL AuthorizedInstall(NSString *srcPath, NSString *dstPath, BOOL *canceled) {
|
||||||
|
if (canceled) *canceled = NO;
|
||||||
|
|
||||||
|
// Make sure that the destination path is an app bundle. We're essentially running 'sudo rm -rf'
|
||||||
|
// so we really don't want to fuck this up.
|
||||||
|
if (![[dstPath pathExtension] isEqualToString:@"app"]) return NO;
|
||||||
|
|
||||||
|
// Do some more checks
|
||||||
|
if ([[dstPath stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) return NO;
|
||||||
|
if ([[srcPath stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]] length] == 0) return NO;
|
||||||
|
|
||||||
|
int pid, status;
|
||||||
|
AuthorizationRef myAuthorizationRef;
|
||||||
|
|
||||||
|
// Get the authorization
|
||||||
|
OSStatus err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &myAuthorizationRef);
|
||||||
|
if (err != errAuthorizationSuccess) return NO;
|
||||||
|
|
||||||
|
AuthorizationItem myItems = {kAuthorizationRightExecute, 0, NULL, 0};
|
||||||
|
AuthorizationRights myRights = {1, &myItems};
|
||||||
|
AuthorizationFlags myFlags = (AuthorizationFlags)(kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights | kAuthorizationFlagPreAuthorize);
|
||||||
|
|
||||||
|
err = AuthorizationCopyRights(myAuthorizationRef, &myRights, NULL, myFlags, NULL);
|
||||||
|
if (err != errAuthorizationSuccess) {
|
||||||
|
if (err == errAuthorizationCanceled && canceled)
|
||||||
|
*canceled = YES;
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
static OSStatus (*security_AuthorizationExecuteWithPrivileges)(AuthorizationRef authorization, const char *pathToTool,
|
||||||
|
AuthorizationFlags options, char * const *arguments,
|
||||||
|
FILE **communicationsPipe) = NULL;
|
||||||
|
if (!security_AuthorizationExecuteWithPrivileges) {
|
||||||
|
// On 10.7, AuthorizationExecuteWithPrivileges is deprecated. We want to still use it since there's no
|
||||||
|
// good alternative (without requiring code signing). We'll look up the function through dyld and fail
|
||||||
|
// if it is no longer accessible. If Apple removes the function entirely this will fail gracefully. If
|
||||||
|
// they keep the function and throw some sort of exception, this won't fail gracefully, but that's a
|
||||||
|
// risk we'll have to take for now.
|
||||||
|
security_AuthorizationExecuteWithPrivileges = (OSStatus (*)(AuthorizationRef, const char*,
|
||||||
|
AuthorizationFlags, char* const*,
|
||||||
|
FILE **)) dlsym(RTLD_DEFAULT, "AuthorizationExecuteWithPrivileges");
|
||||||
|
}
|
||||||
|
if (!security_AuthorizationExecuteWithPrivileges) goto fail;
|
||||||
|
|
||||||
|
// Delete the destination
|
||||||
|
{
|
||||||
|
char *args[] = {"-rf", (char *)[dstPath fileSystemRepresentation], NULL};
|
||||||
|
err = security_AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/bin/rm", kAuthorizationFlagDefaults, args, NULL);
|
||||||
|
if (err != errAuthorizationSuccess) goto fail;
|
||||||
|
|
||||||
|
// Wait until it's done
|
||||||
|
pid = wait(&status);
|
||||||
|
if (pid == -1 || !WIFEXITED(status)) goto fail; // We don't care about exit status as the destination most likely does not exist
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy
|
||||||
|
{
|
||||||
|
char *args[] = {"-pR", (char *)[srcPath fileSystemRepresentation], (char *)[dstPath fileSystemRepresentation], NULL};
|
||||||
|
err = security_AuthorizationExecuteWithPrivileges(myAuthorizationRef, "/bin/cp", kAuthorizationFlagDefaults, args, NULL);
|
||||||
|
if (err != errAuthorizationSuccess) goto fail;
|
||||||
|
|
||||||
|
// Wait until it's done
|
||||||
|
pid = wait(&status);
|
||||||
|
if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status)) goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
|
||||||
|
return YES;
|
||||||
|
|
||||||
|
fail:
|
||||||
|
AuthorizationFree(myAuthorizationRef, kAuthorizationFlagDefaults);
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL CopyBundle(NSString *srcPath, NSString *dstPath) {
|
||||||
|
NSFileManager *fm = [NSFileManager defaultManager];
|
||||||
|
NSError *error = nil;
|
||||||
|
|
||||||
|
if ([fm copyItemAtPath:srcPath toPath:dstPath error:&error]) {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
NSLog(@"ERROR -- Could not copy '%@' to '%@' (%@)", srcPath, dstPath, error);
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSString *ShellQuotedString(NSString *string) {
|
||||||
|
return [NSString stringWithFormat:@"'%@'", [string stringByReplacingOccurrencesOfString:@"'" withString:@"'\\''"]];
|
||||||
|
}
|
||||||
|
|
||||||
|
static void Relaunch(NSString *destinationPath) {
|
||||||
|
// The shell script waits until the original app process terminates.
|
||||||
|
// This is done so that the relaunched app opens as the front-most app.
|
||||||
|
int pid = [[NSProcessInfo processInfo] processIdentifier];
|
||||||
|
|
||||||
|
// Command run just before running open /final/path
|
||||||
|
NSString *preOpenCmd = @"";
|
||||||
|
|
||||||
|
NSString *quotedDestinationPath = ShellQuotedString(destinationPath);
|
||||||
|
|
||||||
|
// OS X >=10.5:
|
||||||
|
// Before we launch the new app, clear xattr:com.apple.quarantine to avoid
|
||||||
|
// duplicate "scary file from the internet" dialog.
|
||||||
|
if (floor(NSAppKitVersionNumber) > NSAppKitVersionNumber10_5) {
|
||||||
|
// Add the -r flag on 10.6
|
||||||
|
preOpenCmd = [NSString stringWithFormat:@"/usr/bin/xattr -d -r com.apple.quarantine %@", quotedDestinationPath];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
preOpenCmd = [NSString stringWithFormat:@"/usr/bin/xattr -d com.apple.quarantine %@", quotedDestinationPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
NSString *script = [NSString stringWithFormat:@"(while /bin/kill -0 %d >&/dev/null; do /bin/sleep 0.1; done; %@; /usr/bin/open %@) &", pid, preOpenCmd, quotedDestinationPath];
|
||||||
|
|
||||||
|
[NSTask launchedTaskWithLaunchPath:@"/bin/sh" arguments:[NSArray arrayWithObjects:@"-c", script, nil]];
|
||||||
|
}
|
||||||
16
distribution/osx/Launcher/src/compile.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# -fobjc-arc: enables ARC
|
||||||
|
# -fmodules: enables modules so you can import with `@import AppKit;`
|
||||||
|
# -mmacosx-version-min=10.6: support older OS X versions, this might increase the binary size
|
||||||
|
|
||||||
|
if [ ! -d "../dist" ]; then mkdir ../dist; fi
|
||||||
|
|
||||||
|
clang PFMoveApplication.m -fno-objc-arc -fmodules -mmacosx-version-min=10.6 -c -o PFMoveApplication.o
|
||||||
|
clang run-with-mono.m Launcher.m PFMoveApplication.o -fobjc-arc -fmodules -mmacosx-version-min=10.6 -o ../dist/Launcher
|
||||||
|
rm PFMoveApplication.o
|
||||||
|
|
||||||
|
if [ "$1" == "install" ] && [ "$2" != "" ]; then
|
||||||
|
echo "Installing to $2"
|
||||||
|
cp ../dist/Launcher $2
|
||||||
|
chmod +x $2
|
||||||
|
fi
|
||||||
11
distribution/osx/Launcher/src/run-with-mono.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@import Foundation;
|
||||||
|
@import AppKit;
|
||||||
|
|
||||||
|
@interface RunWithMono : NSObject {
|
||||||
|
}
|
||||||
|
|
||||||
|
+ (void) openDownloadLink:(NSButton*)button;
|
||||||
|
+ (bool) showDownloadMonoDialog:(NSString *)appName major:(int)major minor:(int)minor;
|
||||||
|
+ (int) runAssemblyWithMono:(NSString *)appName procnamesuffix:(NSString *)procnamesuffix assembly:(NSString *)assembly major:(int) major minor:(int) minor;
|
||||||
|
|
||||||
|
@end
|
||||||
258
distribution/osx/Launcher/src/run-with-mono.m
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
#import "run-with-mono.h"
|
||||||
|
|
||||||
|
@import Foundation;
|
||||||
|
@import AppKit;
|
||||||
|
|
||||||
|
NSString * const VERSION_TITLE = @"Cannot launch %@";
|
||||||
|
NSString * const VERSION_MSG = @"%@ requires the Mono Framework version %d.%d or later.";
|
||||||
|
NSString * const DOWNLOAD_URL = @"http://www.mono-project.com/download/stable/#download-mac";
|
||||||
|
|
||||||
|
// Helper method to see if the user has requested debug output
|
||||||
|
bool D() {
|
||||||
|
NSString* v = [[[NSProcessInfo processInfo]environment]objectForKey:@"DEBUG"];
|
||||||
|
if (v == nil || v.length == 0 || [v isEqual:@"0"] || [v isEqual:@"false"] || [v isEqual:@"f"])
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrapper method to invoke commandline operations and return the string output
|
||||||
|
NSString *runCommand(NSString *program, NSArray<NSString *> *arguments) {
|
||||||
|
NSPipe *pipe = [NSPipe pipe];
|
||||||
|
NSFileHandle *file = pipe.fileHandleForReading;
|
||||||
|
|
||||||
|
NSTask *task = [[NSTask alloc] init];
|
||||||
|
task.launchPath = program;
|
||||||
|
task.arguments = arguments;
|
||||||
|
task.standardOutput = pipe;
|
||||||
|
|
||||||
|
[task launch];
|
||||||
|
|
||||||
|
NSData *data = [file readDataToEndOfFile];
|
||||||
|
[file closeFile];
|
||||||
|
[task waitUntilExit];
|
||||||
|
|
||||||
|
NSString *cmdOutput = [[NSString alloc] initWithData: data encoding: NSUTF8StringEncoding];
|
||||||
|
if (cmdOutput == nil || cmdOutput.length == 0)
|
||||||
|
return nil;
|
||||||
|
|
||||||
|
return [cmdOutput stringByTrimmingCharactersInSet:
|
||||||
|
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the Mono version is greater than or equal to the desired version
|
||||||
|
bool isValidMono(NSString *mono, int major, int minor) {
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
|
||||||
|
if (mono == nil)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (![fileManager fileExistsAtPath:mono] || ![fileManager isExecutableFileAtPath:mono])
|
||||||
|
return false;
|
||||||
|
|
||||||
|
NSString *versionInfo = runCommand(mono, @[@"--version"]);
|
||||||
|
|
||||||
|
NSRange rg = [versionInfo rangeOfString:@"Mono JIT compiler version \\d+\\.\\d+" options:NSRegularExpressionSearch];
|
||||||
|
if (rg.location != NSNotFound) {
|
||||||
|
versionInfo = [versionInfo substringWithRange:rg];
|
||||||
|
if (D()) NSLog(@"Matched version: %@", versionInfo);
|
||||||
|
rg = [versionInfo rangeOfString:@"\\d+\\.\\d+" options:NSRegularExpressionSearch];
|
||||||
|
if (rg.location != NSNotFound) {
|
||||||
|
versionInfo = [versionInfo substringWithRange:rg];
|
||||||
|
if (D()) NSLog(@"Matched version: %@", versionInfo);
|
||||||
|
|
||||||
|
NSArray<NSString *> *versionComponents = [versionInfo componentsSeparatedByString:@"."];
|
||||||
|
if ([versionComponents[0] intValue] < major)
|
||||||
|
return false;
|
||||||
|
if ([versionComponents[0] intValue] == major && [versionComponents[1] intValue] < minor)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to locate a mono with a valid version
|
||||||
|
NSString *findMono(int major, int minor) {
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
|
||||||
|
NSString *currentMono = runCommand(@"/usr/bin/which", @[@"mono"]);
|
||||||
|
if (D()) NSLog(@"which mono: %@", currentMono);
|
||||||
|
|
||||||
|
if (isValidMono(currentMono, major, minor)) {
|
||||||
|
if (D()) NSLog(@"Found mono with: %@", currentMono);
|
||||||
|
return currentMono;
|
||||||
|
}
|
||||||
|
|
||||||
|
NSArray *probepaths = @[@"/usr/local/bin/mono", @"/Library/Frameworks/Mono.framework/Versions/Current/bin/mono", @"/opt/local/bin/mono"];
|
||||||
|
for(NSString* probepath in probepaths) {
|
||||||
|
if (D()) NSLog(@"Trying mono with: %@", probepath);
|
||||||
|
if (isValidMono(probepath, major, minor)) {
|
||||||
|
if (D()) NSLog(@"Found mono with: %@", probepath);
|
||||||
|
return probepath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (D()) NSLog(@"Failed to find Mono, returning: %@", nil);
|
||||||
|
return nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Bundle for quarantine
|
||||||
|
void checkBundle() {
|
||||||
|
|
||||||
|
NSString * const bundlePath = [[NSBundle mainBundle] bundlePath];
|
||||||
|
NSString * const attributes = runCommand(@"/usr/bin/xattr", @[@"-l", bundlePath]);
|
||||||
|
if (D()) NSLog(@"Attributes: %@", attributes);
|
||||||
|
if ([attributes containsString:@"com.apple.quarantine:"]) {
|
||||||
|
runCommand(@"/usr/bin/xattr", @[@"-dr", @"com.apple.quarantine", bundlePath]);
|
||||||
|
NSLog(@"Removed quarantine attribute from bundle");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@implementation RunWithMono
|
||||||
|
|
||||||
|
+ (void) openDownloadLink:(NSButton*)button {
|
||||||
|
if (D()) NSLog(@"Clicked Download");
|
||||||
|
runCommand(@"/usr/bin/open", @[DOWNLOAD_URL]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shows the download dialog, prompting to download Mono
|
||||||
|
+ (bool) showDownloadMonoDialog:(NSString *)appName major:(int)major minor:(int)minor {
|
||||||
|
NSAlert *alert = [[NSAlert alloc] init];
|
||||||
|
|
||||||
|
[alert setInformativeText:[NSString stringWithFormat:VERSION_MSG, appName, major, minor]];
|
||||||
|
[alert setMessageText:[NSString stringWithFormat:VERSION_TITLE, appName]];
|
||||||
|
[alert addButtonWithTitle:@"Cancel"];
|
||||||
|
[alert addButtonWithTitle:@"Retry"];
|
||||||
|
[alert addButtonWithTitle:@"Download"];
|
||||||
|
|
||||||
|
NSButton *downloadButton = [[alert buttons] objectAtIndex:2];
|
||||||
|
|
||||||
|
[downloadButton setTarget:self];
|
||||||
|
[downloadButton setAction:@selector(openDownloadLink:)];
|
||||||
|
|
||||||
|
NSModalResponse btn = [alert runModal];
|
||||||
|
if (btn == NSAlertFirstButtonReturn) {
|
||||||
|
if (D()) NSLog(@"Clicked Cancel");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else if (btn == NSAlertSecondButtonReturn) {
|
||||||
|
if (D()) NSLog(@"Clicked Retry");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top-level method, finds Mono with an appropriate version and launches the assembly
|
||||||
|
+ (int) runAssemblyWithMono: (NSString *)appName procnamesuffix:(NSString *)procnamesuffix assembly:(NSString *)assembly major:(int) major minor:(int) minor {
|
||||||
|
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||||||
|
|
||||||
|
NSString *assemblyPath;
|
||||||
|
bool found = false;
|
||||||
|
|
||||||
|
NSString *localPath = NSProcessInfo.processInfo.arguments[0].stringByDeletingLastPathComponent;
|
||||||
|
NSString *resourcePath = [[NSBundle mainBundle] resourcePath];
|
||||||
|
NSArray *paths = @[
|
||||||
|
localPath,
|
||||||
|
[NSString pathWithComponents:@[localPath, @"bin"]],
|
||||||
|
resourcePath,
|
||||||
|
[NSString pathWithComponents:@[resourcePath, @"bin"]]
|
||||||
|
];
|
||||||
|
for (NSString* entryFolder in paths) {
|
||||||
|
if (D()) NSLog(@"Checking folder: %@", entryFolder);
|
||||||
|
|
||||||
|
assemblyPath = [NSString pathWithComponents:@[entryFolder, assembly]];
|
||||||
|
|
||||||
|
if ([fileManager fileExistsAtPath:assemblyPath]) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
NSLog(@"Assembly file not found");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (D()) NSLog(@"assemblyPath: %@", assemblyPath);
|
||||||
|
|
||||||
|
checkBundle();
|
||||||
|
|
||||||
|
NSString *currentMono = findMono(major, minor);
|
||||||
|
|
||||||
|
while (currentMono == nil) {
|
||||||
|
NSLog(@"No valid mono found!");
|
||||||
|
bool close = [self showDownloadMonoDialog:appName major:major minor:minor];
|
||||||
|
if (close)
|
||||||
|
return 1;
|
||||||
|
currentMono = findMono(major, minor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup dylib fallback loading
|
||||||
|
NSMutableArray * dylibPath = [NSMutableArray arrayWithObject:assemblyPath.stringByDeletingLastPathComponent];
|
||||||
|
|
||||||
|
// Update the PATH to use the specified mono version
|
||||||
|
if ([currentMono hasPrefix:@"/"])
|
||||||
|
{
|
||||||
|
NSString * curMonoBinDir = currentMono.stringByDeletingLastPathComponent;
|
||||||
|
NSString * curMonoDir = curMonoBinDir.stringByDeletingLastPathComponent;
|
||||||
|
NSString * curMonoLibDir = [NSString pathWithComponents:@[curMonoDir, @"lib"]];
|
||||||
|
|
||||||
|
NSString * curEnvPath = [NSString stringWithUTF8String:getenv("PATH")];
|
||||||
|
NSString * newEnvPath = [NSString stringWithFormat:@"%@:%@", curMonoBinDir, curEnvPath];
|
||||||
|
setenv("PATH", newEnvPath.UTF8String, 1);
|
||||||
|
|
||||||
|
[dylibPath addObject:curMonoLibDir];
|
||||||
|
|
||||||
|
NSLog(@"Added %@ to PATH", curMonoBinDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup libsqlite?
|
||||||
|
/* if [[ -f '/opt/local/lib/libsqlite3.0.dylib' ]]; then
|
||||||
|
export DYLD_FALLBACK_LIBRARY_PATH="/opt/local/lib:$DYLD_FALLBACK_LIBRARY_PATH"
|
||||||
|
fi
|
||||||
|
*/
|
||||||
|
|
||||||
|
[dylibPath addObjectsFromArray:@[@"$HOME/lib", @"/usr/local/lib", @"/lib", @"/usr/lib"]];
|
||||||
|
|
||||||
|
setenv("DYLD_FALLBACK_LIBRARY_PATH", [dylibPath componentsJoinedByString:@":"].UTF8String, 1);
|
||||||
|
|
||||||
|
if (D()) NSLog(@"Running %@ --debug %@", currentMono, assemblyPath);
|
||||||
|
|
||||||
|
// Copy commandline arguments
|
||||||
|
NSMutableArray* arguments = [[NSMutableArray alloc] init];
|
||||||
|
// Disabled suffix for now coz it's confusing and not preserved on in-app restart
|
||||||
|
[arguments addObject:currentMono];
|
||||||
|
//[arguments addObject:[currentMono stringByAppendingString:procnamesuffix]];
|
||||||
|
[arguments addObject:@"--debug"];
|
||||||
|
[arguments addObjectsFromArray:[[NSProcessInfo processInfo] arguments]];
|
||||||
|
|
||||||
|
// replace the executable-path with the assembly path
|
||||||
|
[arguments replaceObjectAtIndex:2 withObject:assemblyPath];
|
||||||
|
|
||||||
|
// Try switch to mono using execv
|
||||||
|
char * cPath = strdup([currentMono UTF8String]);
|
||||||
|
char ** cArgs;
|
||||||
|
char ** pArgNext = cArgs = malloc(sizeof(*cArgs) * ([arguments count] + 1));
|
||||||
|
for (NSString *s in arguments) {
|
||||||
|
*pArgNext++ = strdup([s UTF8String]);
|
||||||
|
}
|
||||||
|
*pArgNext = NULL;
|
||||||
|
int ret = execv(cPath, cArgs);
|
||||||
|
if (ret != 0)
|
||||||
|
NSLog(@"Failed execv with errno @d", errno);
|
||||||
|
// execv failed, cleanup
|
||||||
|
pArgNext = cArgs;
|
||||||
|
for (NSString *s in arguments) {
|
||||||
|
free(*pArgNext++);
|
||||||
|
}
|
||||||
|
free(cArgs);
|
||||||
|
free(cPath);
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
@@ -4,4 +4,4 @@ echo ##teamcity[progressStart 'Building setup file']
|
|||||||
inno\ISCC.exe sonarr.iss
|
inno\ISCC.exe sonarr.iss
|
||||||
echo ##teamcity[progressFinish 'Building setup file']
|
echo ##teamcity[progressFinish 'Building setup file']
|
||||||
|
|
||||||
echo ##teamcity[publishArtifacts 'setup\output\*.exe']
|
echo ##teamcity[publishArtifacts 'distribution\windows\setup\output\*.exe']
|
||||||
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
@@ -37,19 +37,20 @@ Compression=lzma2/normal
|
|||||||
AppContact={#ForumsURL}
|
AppContact={#ForumsURL}
|
||||||
VersionInfoVersion={#BuildNumber}
|
VersionInfoVersion={#BuildNumber}
|
||||||
SetupLogging=yes
|
SetupLogging=yes
|
||||||
|
OutputDir=output
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
[Tasks]
|
[Tasks]
|
||||||
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
|
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
|
||||||
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||||
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive
|
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive
|
||||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||||
|
|
||||||
[Files]
|
[Files]
|
||||||
Source: "..\_output_windows\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
Source: "..\..\..\_output_windows\Sonarr.exe"; DestDir: "{app}"; Flags: ignoreversion
|
||||||
Source: "..\_output_windows\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
Source: "..\..\..\_output_windows\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
@@ -59,11 +60,12 @@ Name: "{userstartup}\{#AppName}"; Filename: "{app}\Sonarr.exe"; WorkingDir: "{ap
|
|||||||
|
|
||||||
[InstallDelete]
|
[InstallDelete]
|
||||||
Name: "{commonappdata}\NzbDrone\bin"; Type: filesandordirs
|
Name: "{commonappdata}\NzbDrone\bin"; Type: filesandordirs
|
||||||
|
Name: "{app}"; Type: filesandordirs
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated;
|
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
|
||||||
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
|
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
|
||||||
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i"; Flags: runhidden waituntilterminated; Tasks: windowsService
|
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
|
||||||
Filename: "{app}\Sonarr.exe"; Description: "Open Sonarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
|
Filename: "{app}\Sonarr.exe"; Description: "Open Sonarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
|
||||||
Filename: "{app}\Sonarr.exe"; Description: "Start Sonarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
|
Filename: "{app}\Sonarr.exe"; Description: "Start Sonarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;
|
||||||
|
|
||||||
@@ -75,7 +77,8 @@ function PrepareToInstall(var NeedsRestart: Boolean): String;
|
|||||||
var
|
var
|
||||||
ResultCode: Integer;
|
ResultCode: Integer;
|
||||||
begin
|
begin
|
||||||
Exec(ExpandConstant('{commonappdata}\NzbDrone\bin\NzbDrone.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode)
|
Exec('net', 'stop nzbdrone', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||||
|
Exec('sc', 'delete nzbdrone', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function Framework472IsNotInstalled(): Boolean;
|
function Framework472IsNotInstalled(): Boolean;
|
||||||
@@ -17,6 +17,9 @@ RUN fromdos /startup.sh
|
|||||||
|
|
||||||
WORKDIR /data/
|
WORKDIR /data/
|
||||||
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||||
|
|
||||||
|
RUN groupadd sonarrtst -g 4020 && useradd sonarrtst -u 4021 -g 4020 -m -s /bin/bash
|
||||||
|
USER sonarrtst
|
||||||
|
|
||||||
CMD bash /startup.sh
|
CMD bash /startup.sh
|
||||||
|
|
||||||
|
|||||||
@@ -25,5 +25,8 @@ RUN fromdos /startup.sh
|
|||||||
WORKDIR /data/
|
WORKDIR /data/
|
||||||
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
|
||||||
|
|
||||||
|
RUN groupadd sonarrtst -g 4020 && useradd sonarrtst -u 4021 -g 4020 -m -s /bin/bash
|
||||||
|
USER sonarrtst
|
||||||
|
|
||||||
CMD bash /startup.sh
|
CMD bash /startup.sh
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ gulp.task('build',
|
|||||||
'webpack',
|
'webpack',
|
||||||
'copyHtml',
|
'copyHtml',
|
||||||
'copyFonts',
|
'copyFonts',
|
||||||
'copyImages'
|
'copyImages',
|
||||||
|
'copyRobots'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,3 +32,11 @@ gulp.task('copyImages', () => {
|
|||||||
.pipe(gulp.dest(paths.dest.root))
|
.pipe(gulp.dest(paths.dest.root))
|
||||||
.pipe(livereload());
|
.pipe(livereload());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
gulp.task('copyRobots', () => {
|
||||||
|
return gulp.src(paths.src.robots, { base: paths.src.root })
|
||||||
|
.pipe(cache('copyRobots'))
|
||||||
|
.pipe(print())
|
||||||
|
.pipe(gulp.dest(paths.dest.root))
|
||||||
|
.pipe(livereload());
|
||||||
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const paths = {
|
|||||||
content: `${root}/Content/`,
|
content: `${root}/Content/`,
|
||||||
fonts: `${root}/Content/Fonts/`,
|
fonts: `${root}/Content/Fonts/`,
|
||||||
images: `${root}/Content/Images/`,
|
images: `${root}/Content/Images/`,
|
||||||
|
robots: `${root}/Content/robots.txt`,
|
||||||
exclude: {
|
exclude: {
|
||||||
libs: `!${root}/JsLibraries/**`
|
libs: `!${root}/JsLibraries/**`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function watch() {
|
|||||||
gulpWatch(paths.src.html, gulp.series('copyHtml'));
|
gulpWatch(paths.src.html, gulp.series('copyHtml'));
|
||||||
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
|
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
|
||||||
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
|
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
|
||||||
|
gulpWatch(paths.src.robots, gulp.series('copyRobots'));
|
||||||
}
|
}
|
||||||
|
|
||||||
gulp.task('watch', gulp.series('build', watch));
|
gulp.task('watch', gulp.series('build', watch));
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ 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 HtmlWebpackPlugin = require('html-webpack-plugin');
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
|
||||||
const TerserPlugin = require('terser-webpack-plugin');
|
const TerserPlugin = require('terser-webpack-plugin');
|
||||||
|
|
||||||
const uiFolder = 'UI';
|
const uiFolder = 'UI';
|
||||||
@@ -13,7 +14,7 @@ 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 isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
|
||||||
const inlineWebWorkers = true;
|
const inlineWebWorkers = 'no-fallback';
|
||||||
|
|
||||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||||
|
|
||||||
@@ -31,14 +32,19 @@ const cssVarsFiles = [
|
|||||||
].map(require.resolve);
|
].map(require.resolve);
|
||||||
|
|
||||||
// Override the way HtmlWebpackPlugin injects the scripts
|
// Override the way HtmlWebpackPlugin injects the scripts
|
||||||
|
// TODO: Find a better way to get these paths without
|
||||||
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
||||||
const head = assetTags.head.map((v) => {
|
const head = assetTags.headTags.map((v) => {
|
||||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
|
const href = v.attributes.href
|
||||||
return this.createHtmlTag(v);
|
.replace('\\', '/')
|
||||||
|
.replace('%5C', '/');
|
||||||
|
|
||||||
|
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
|
||||||
|
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||||
});
|
});
|
||||||
const body = assetTags.body.map((v) => {
|
const body = assetTags.bodyTags.map((v) => {
|
||||||
v.attributes = { src: `/${v.attributes.src}` };
|
v.attributes = { src: `/${v.attributes.src}` };
|
||||||
return this.createHtmlTag(v);
|
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||||
});
|
});
|
||||||
|
|
||||||
return html
|
return html
|
||||||
@@ -119,17 +125,11 @@ const config = {
|
|||||||
rules: [
|
rules: [
|
||||||
{
|
{
|
||||||
test: /\.worker\.js$/,
|
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: {
|
use: {
|
||||||
loader: 'worker-loader',
|
loader: 'worker-loader',
|
||||||
options: {
|
options: {
|
||||||
name: '[name].js',
|
filename: '[name].js',
|
||||||
inline: inlineWebWorkers,
|
inline: inlineWebWorkers
|
||||||
fallback: !inlineWebWorkers
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { align, icons } from 'Helpers/Props';
|
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
@@ -15,6 +22,72 @@ import BlacklistRowConnector from './BlacklistRowConnector';
|
|||||||
|
|
||||||
class Blacklist extends Component {
|
class Blacklist extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
allSelected: false,
|
||||||
|
allUnselected: false,
|
||||||
|
lastToggled: null,
|
||||||
|
selectedState: {},
|
||||||
|
isConfirmRemoveModalOpen: false,
|
||||||
|
items: props.items
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
items
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (hasDifferentItems(prevProps.items, items)) {
|
||||||
|
this.setState((state) => {
|
||||||
|
return {
|
||||||
|
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||||
|
items
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
getSelectedIds = () => {
|
||||||
|
return getSelectedIds(this.state.selectedState);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onSelectAllChange = ({ value }) => {
|
||||||
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||||
|
this.setState((state) => {
|
||||||
|
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveSelectedPress = () => {
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemoveSelectedConfirmed = () => {
|
||||||
|
this.props.onRemoveSelected(this.getSelectedIds());
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmRemoveModalClose = () => {
|
||||||
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -26,15 +99,33 @@ class Blacklist extends Component {
|
|||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
|
isRemoving,
|
||||||
isClearingBlacklistExecuting,
|
isClearingBlacklistExecuting,
|
||||||
onClearBlacklistPress,
|
onClearBlacklistPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
allSelected,
|
||||||
|
allUnselected,
|
||||||
|
selectedState,
|
||||||
|
isConfirmRemoveModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const selectedIds = this.getSelectedIds();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title="Blacklist">
|
<PageContent title="Blacklist">
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Remove Selected"
|
||||||
|
iconName={icons.REMOVE}
|
||||||
|
isDisabled={!selectedIds.length}
|
||||||
|
isSpinning={isRemoving}
|
||||||
|
onPress={this.onRemoveSelectedPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Clear"
|
label="Clear"
|
||||||
iconName={icons.CLEAR}
|
iconName={icons.CLEAR}
|
||||||
@@ -59,51 +150,67 @@ class Blacklist extends Component {
|
|||||||
<PageContentBody>
|
<PageContentBody>
|
||||||
{
|
{
|
||||||
isFetching && !isPopulated &&
|
isFetching && !isPopulated &&
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<div>Unable to load blacklist</div>
|
<div>Unable to load blacklist</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !items.length &&
|
isPopulated && !error && !items.length &&
|
||||||
<div>
|
<div>
|
||||||
No history blacklist
|
No history blacklist
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !!items.length &&
|
isPopulated && !error && !!items.length &&
|
||||||
<div>
|
<div>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
selectAll={true}
|
||||||
{...otherProps}
|
allSelected={allSelected}
|
||||||
>
|
allUnselected={allUnselected}
|
||||||
<TableBody>
|
columns={columns}
|
||||||
{
|
{...otherProps}
|
||||||
items.map((item) => {
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
return (
|
>
|
||||||
<BlacklistRowConnector
|
<TableBody>
|
||||||
key={item.id}
|
{
|
||||||
columns={columns}
|
items.map((item) => {
|
||||||
{...item}
|
return (
|
||||||
/>
|
<BlacklistRowConnector
|
||||||
);
|
key={item.id}
|
||||||
})
|
isSelected={selectedState[item.id] || false}
|
||||||
}
|
columns={columns}
|
||||||
</TableBody>
|
{...item}
|
||||||
</Table>
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
<TablePager
|
<TablePager
|
||||||
totalRecords={totalRecords}
|
totalRecords={totalRecords}
|
||||||
isFetching={isFetching}
|
isFetching={isFetching}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Remove Selected"
|
||||||
|
message={'Are you sure you want to remove the selected items from the blacklist?'}
|
||||||
|
confirmLabel="Remove Selected"
|
||||||
|
onConfirm={this.onRemoveSelectedConfirmed}
|
||||||
|
onCancel={this.onConfirmRemoveModalClose}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -116,7 +223,9 @@ Blacklist.propTypes = {
|
|||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||||
|
onRemoveSelected: PropTypes.func.isRequired,
|
||||||
onClearBlacklistPress: PropTypes.func.isRequired
|
onClearBlacklistPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ class BlacklistConnector extends Component {
|
|||||||
this.props.gotoBlacklistPage({ page });
|
this.props.gotoBlacklistPage({ page });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRemoveSelected = (ids) => {
|
||||||
|
this.props.removeBlacklistItems({ ids });
|
||||||
|
}
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
onSortPress = (sortKey) => {
|
||||||
this.props.setBlacklistSort({ sortKey });
|
this.props.setBlacklistSort({ sortKey });
|
||||||
}
|
}
|
||||||
@@ -124,6 +128,7 @@ class BlacklistConnector extends Component {
|
|||||||
onNextPagePress={this.onNextPagePress}
|
onNextPagePress={this.onNextPagePress}
|
||||||
onLastPagePress={this.onLastPagePress}
|
onLastPagePress={this.onLastPagePress}
|
||||||
onPageSelect={this.onPageSelect}
|
onPageSelect={this.onPageSelect}
|
||||||
|
onRemoveSelected={this.onRemoveSelected}
|
||||||
onSortPress={this.onSortPress}
|
onSortPress={this.onSortPress}
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||||
@@ -143,6 +148,7 @@ BlacklistConnector.propTypes = {
|
|||||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||||
|
removeBlacklistItems: PropTypes.func.isRequired,
|
||||||
setBlacklistSort: PropTypes.func.isRequired,
|
setBlacklistSort: PropTypes.func.isRequired,
|
||||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||||
clearBlacklist: PropTypes.func.isRequired,
|
clearBlacklist: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
@@ -40,6 +41,7 @@ class BlacklistRow extends Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
series,
|
series,
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
language,
|
language,
|
||||||
@@ -48,12 +50,20 @@ class BlacklistRow extends Component {
|
|||||||
protocol,
|
protocol,
|
||||||
indexer,
|
indexer,
|
||||||
message,
|
message,
|
||||||
|
isSelected,
|
||||||
columns,
|
columns,
|
||||||
|
onSelectedChange,
|
||||||
onRemovePress
|
onRemovePress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
<TableSelectCell
|
||||||
|
id={id}
|
||||||
|
isSelected={isSelected}
|
||||||
|
onSelectedChange={onSelectedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
columns.map((column) => {
|
columns.map((column) => {
|
||||||
const {
|
const {
|
||||||
@@ -179,7 +189,9 @@ BlacklistRow.propTypes = {
|
|||||||
protocol: PropTypes.string.isRequired,
|
protocol: PropTypes.string.isRequired,
|
||||||
indexer: PropTypes.string,
|
indexer: PropTypes.string,
|
||||||
message: PropTypes.string,
|
message: PropTypes.string,
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
onRemovePress: PropTypes.func.isRequired
|
onRemovePress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
|
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
|
||||||
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||||
import BlacklistRow from './BlacklistRow';
|
import BlacklistRow from './BlacklistRow';
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
|||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
onRemovePress() {
|
onRemovePress() {
|
||||||
dispatch(removeFromBlacklist({ id: props.id }));
|
dispatch(removeBlacklistItem({ id: props.id }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
@@ -22,6 +23,7 @@ function HistoryDetails(props) {
|
|||||||
const {
|
const {
|
||||||
indexer,
|
indexer,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
|
preferredWordScore,
|
||||||
nzbInfoUrl,
|
nzbInfoUrl,
|
||||||
downloadClient,
|
downloadClient,
|
||||||
downloadId,
|
downloadId,
|
||||||
@@ -40,24 +42,35 @@ function HistoryDetails(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!indexer &&
|
indexer ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title="Indexer"
|
title="Indexer"
|
||||||
data={indexer}
|
data={indexer}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!releaseGroup &&
|
releaseGroup ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
title="Release Group"
|
title="Release Group"
|
||||||
data={releaseGroup}
|
data={releaseGroup}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!nzbInfoUrl &&
|
preferredWordScore && preferredWordScore !== '0' ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Preferred Word Score"
|
||||||
|
data={formatPreferredWordScore(preferredWordScore)}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
nzbInfoUrl ?
|
||||||
<span>
|
<span>
|
||||||
<DescriptionListItemTitle>
|
<DescriptionListItemTitle>
|
||||||
Info URL
|
Info URL
|
||||||
@@ -66,39 +79,44 @@ function HistoryDetails(props) {
|
|||||||
<DescriptionListItemDescription>
|
<DescriptionListItemDescription>
|
||||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||||
</DescriptionListItemDescription>
|
</DescriptionListItemDescription>
|
||||||
</span>
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!downloadClient &&
|
downloadClient ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title="Download Client"
|
title="Download Client"
|
||||||
data={downloadClient}
|
data={downloadClient}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!downloadId &&
|
downloadId ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title="Grab ID"
|
title="Grab ID"
|
||||||
data={downloadId}
|
data={downloadId}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!indexer &&
|
age || ageHours || ageMinutes ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title="Age (when grabbed)"
|
title="Age (when grabbed)"
|
||||||
data={formatAge(age, ageHours, ageMinutes)}
|
data={formatAge(age, ageHours, ageMinutes)}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!publishedDate &&
|
publishedDate ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title="Published Date"
|
title="Published Date"
|
||||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
@@ -118,11 +136,12 @@ function HistoryDetails(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!message &&
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title="Message"
|
title="Message"
|
||||||
data={message}
|
data={message}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
@@ -130,6 +149,7 @@ function HistoryDetails(props) {
|
|||||||
|
|
||||||
if (eventType === 'downloadFolderImported') {
|
if (eventType === 'downloadFolderImported') {
|
||||||
const {
|
const {
|
||||||
|
preferredWordScore,
|
||||||
droppedPath,
|
droppedPath,
|
||||||
importedPath
|
importedPath
|
||||||
} = data;
|
} = data;
|
||||||
@@ -143,21 +163,32 @@ function HistoryDetails(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!droppedPath &&
|
droppedPath ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
title="Source"
|
title="Source"
|
||||||
data={droppedPath}
|
data={droppedPath}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!importedPath &&
|
importedPath ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
descriptionClassName={styles.description}
|
descriptionClassName={styles.description}
|
||||||
title="Imported To"
|
title="Imported To"
|
||||||
data={importedPath}
|
data={importedPath}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
preferredWordScore && preferredWordScore !== '0' ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Preferred Word Score"
|
||||||
|
data={formatPreferredWordScore(preferredWordScore)}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
@@ -165,7 +196,8 @@ function HistoryDetails(props) {
|
|||||||
|
|
||||||
if (eventType === 'episodeFileDeleted') {
|
if (eventType === 'episodeFileDeleted') {
|
||||||
const {
|
const {
|
||||||
reason
|
reason,
|
||||||
|
preferredWordScore
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
let reasonMessage = '';
|
let reasonMessage = '';
|
||||||
@@ -195,6 +227,15 @@ function HistoryDetails(props) {
|
|||||||
title="Reason"
|
title="Reason"
|
||||||
data={reasonMessage}
|
data={reasonMessage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
preferredWordScore && preferredWordScore !== '0' ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Preferred Word Score"
|
||||||
|
data={formatPreferredWordScore(preferredWordScore)}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -246,11 +287,12 @@ function HistoryDetails(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!message &&
|
message ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title="Message"
|
title="Message"
|
||||||
data={message}
|
data={message}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
width: 80px;
|
width: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preferredWordScore {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
.releaseGroup {
|
.releaseGroup {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
@@ -194,6 +195,17 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'preferredWordScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.preferredWordScore}
|
||||||
|
>
|
||||||
|
{formatPreferredWordScore(data.preferredWordScore)}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'releaseGroup') {
|
if (name === 'releaseGroup') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ class Queue extends Component {
|
|||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
|
this._shouldBlockRefresh = false;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
allSelected: false,
|
allSelected: false,
|
||||||
allUnselected: false,
|
allUnselected: false,
|
||||||
@@ -42,6 +44,14 @@ class Queue extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate() {
|
||||||
|
if (this._shouldBlockRefresh) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
@@ -82,6 +92,10 @@ class Queue extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
onQueueRowModalOpenOrClose = (isOpen) => {
|
||||||
|
this._shouldBlockRefresh = isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
onSelectAllChange = ({ value }) => {
|
onSelectAllChange = ({ value }) => {
|
||||||
this.setState(selectAll(this.state.selectedState, value));
|
this.setState(selectAll(this.state.selectedState, value));
|
||||||
}
|
}
|
||||||
@@ -97,15 +111,19 @@ class Queue extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onRemoveSelectedPress = () => {
|
onRemoveSelectedPress = () => {
|
||||||
this.setState({ isConfirmRemoveModalOpen: true });
|
this.setState({ isConfirmRemoveModalOpen: true }, () => {
|
||||||
|
this._shouldBlockRefresh = true;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveSelectedConfirmed = (payload) => {
|
onRemoveSelectedConfirmed = (payload) => {
|
||||||
|
this._shouldBlockRefresh = false;
|
||||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onConfirmRemoveModalClose = () => {
|
onConfirmRemoveModalClose = () => {
|
||||||
|
this._shouldBlockRefresh = false;
|
||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +223,7 @@ class Queue extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !hasError && !items.length &&
|
isAllPopulated && !hasError && !items.length &&
|
||||||
<div>
|
<div>
|
||||||
Queue is empty
|
Queue is empty
|
||||||
</div>
|
</div>
|
||||||
@@ -234,6 +252,7 @@ class Queue extends Component {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
{...item}
|
{...item}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
|
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,19 +42,32 @@ class QueueRow extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onRemoveQueueItemModalConfirmed = (blacklist) => {
|
onRemoveQueueItemModalConfirmed = (blacklist) => {
|
||||||
this.props.onRemoveQueueItemPress(blacklist);
|
const {
|
||||||
|
onRemoveQueueItemPress,
|
||||||
|
onQueueRowModalOpenOrClose
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onQueueRowModalOpenOrClose(false);
|
||||||
|
onRemoveQueueItemPress(blacklist);
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onRemoveQueueItemModalClose = () => {
|
onRemoveQueueItemModalClose = () => {
|
||||||
|
this.props.onQueueRowModalOpenOrClose(false);
|
||||||
|
|
||||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
onInteractiveImportPress = () => {
|
onInteractiveImportPress = () => {
|
||||||
|
this.props.onQueueRowModalOpenOrClose(true);
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: true });
|
this.setState({ isInteractiveImportModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onInteractiveImportModalClose = () => {
|
onInteractiveImportModalClose = () => {
|
||||||
|
this.props.onQueueRowModalOpenOrClose(false);
|
||||||
|
|
||||||
this.setState({ isInteractiveImportModalOpen: false });
|
this.setState({ isInteractiveImportModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,7 +410,8 @@ QueueRow.propTypes = {
|
|||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
onGrabPress: PropTypes.func.isRequired,
|
onGrabPress: PropTypes.func.isRequired,
|
||||||
onRemoveQueueItemPress: PropTypes.func.isRequired
|
onRemoveQueueItemPress: PropTypes.func.isRequired,
|
||||||
|
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
QueueRow.defaultProps = {
|
QueueRow.defaultProps = {
|
||||||
|
|||||||
@@ -3,3 +3,7 @@
|
|||||||
|
|
||||||
width: 30px;
|
width: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noMessages {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ function getDetailedPopoverBody(statusMessages) {
|
|||||||
{
|
{
|
||||||
statusMessages.map(({ title, messages }) => {
|
statusMessages.map(({ title, messages }) => {
|
||||||
return (
|
return (
|
||||||
<div key={title}>
|
<div
|
||||||
|
key={title}
|
||||||
|
className={messages.length ? undefined: styles.noMessages}
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
<ul>
|
<ul>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ class AddNewSeries extends Component {
|
|||||||
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
|
<div className={styles.noResults}>Couldn't find any results for '{term}'</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>
|
||||||
<Link to="https://github.com/Sonarr/Sonarr/wiki/FAQ#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
|
<Link to="https://wiki.servarr.com/Sonarr_FAQ#Why_cant_I_add_a_new_series_when_I_know_the_TVDB_ID">
|
||||||
Why can't I find my show?
|
Why can't I find my show?
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,23 +25,24 @@
|
|||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchForMissingEpisodesLabelContainer {
|
.searchLabelContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchForMissingEpisodesLabel {
|
.searchLabel {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchForMissingEpisodesContainer {
|
.searchInputContainer {
|
||||||
composes: container from '~Components/Form/CheckInput.css';
|
composes: container from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
flex: 0 1 0;
|
flex: 0 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.searchForMissingEpisodesInput {
|
.searchInput {
|
||||||
composes: input from '~Components/Form/CheckInput.css';
|
composes: input from '~Components/Form/CheckInput.css';
|
||||||
|
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
|
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
|
||||||
props.seriesType.value :
|
props.seriesType.value :
|
||||||
props.initialSeriesType,
|
props.initialSeriesType
|
||||||
searchForMissingEpisodes: false
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +43,6 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onSearchForMissingEpisodesChange = ({ value }) => {
|
|
||||||
this.setState({ searchForMissingEpisodes: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onQualityProfileIdChange = ({ value }) => {
|
onQualityProfileIdChange = ({ value }) => {
|
||||||
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
|
||||||
}
|
}
|
||||||
@@ -58,11 +53,12 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
|
|
||||||
onAddSeriesPress = () => {
|
onAddSeriesPress = () => {
|
||||||
const {
|
const {
|
||||||
searchForMissingEpisodes,
|
|
||||||
seriesType
|
seriesType
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
this.props.onAddSeriesPress(searchForMissingEpisodes, seriesType);
|
this.props.onAddSeriesPress(
|
||||||
|
seriesType
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -81,6 +77,8 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
languageProfileId,
|
languageProfileId,
|
||||||
seriesType,
|
seriesType,
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
|
searchForMissingEpisodes,
|
||||||
|
searchForCutoffUnmetEpisodes,
|
||||||
folder,
|
folder,
|
||||||
tags,
|
tags,
|
||||||
showLanguageProfile,
|
showLanguageProfile,
|
||||||
@@ -246,19 +244,35 @@ class AddNewSeriesModalContent extends Component {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter className={styles.modalFooter}>
|
<ModalFooter className={styles.modalFooter}>
|
||||||
<label className={styles.searchForMissingEpisodesLabelContainer}>
|
<div>
|
||||||
<span className={styles.searchForMissingEpisodesLabel}>
|
<label className={styles.searchLabelContainer}>
|
||||||
Start search for missing episodes
|
<span className={styles.searchLabel}>
|
||||||
</span>
|
Start search for missing episodes
|
||||||
|
</span>
|
||||||
|
|
||||||
<CheckInput
|
<CheckInput
|
||||||
containerClassName={styles.searchForMissingEpisodesContainer}
|
containerClassName={styles.searchInputContainer}
|
||||||
className={styles.searchForMissingEpisodesInput}
|
className={styles.searchInput}
|
||||||
name="searchForMissingEpisodes"
|
name="searchForMissingEpisodes"
|
||||||
value={this.state.searchForMissingEpisodes}
|
onChange={onInputChange}
|
||||||
onChange={this.onSearchForMissingEpisodesChange}
|
{...searchForMissingEpisodes}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label className={styles.searchLabelContainer}>
|
||||||
|
<span className={styles.searchLabel}>
|
||||||
|
Start search for cutoff unmet episodes
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<CheckInput
|
||||||
|
containerClassName={styles.searchInputContainer}
|
||||||
|
className={styles.searchInput}
|
||||||
|
name="searchForCutoffUnmetEpisodes"
|
||||||
|
onChange={onInputChange}
|
||||||
|
{...searchForCutoffUnmetEpisodes}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SpinnerButton
|
<SpinnerButton
|
||||||
className={styles.addButton}
|
className={styles.addButton}
|
||||||
@@ -288,6 +302,8 @@ AddNewSeriesModalContent.propTypes = {
|
|||||||
languageProfileId: PropTypes.object,
|
languageProfileId: PropTypes.object,
|
||||||
seriesType: PropTypes.object.isRequired,
|
seriesType: PropTypes.object.isRequired,
|
||||||
seasonFolder: PropTypes.object.isRequired,
|
seasonFolder: PropTypes.object.isRequired,
|
||||||
|
searchForMissingEpisodes: PropTypes.object.isRequired,
|
||||||
|
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
|
||||||
folder: PropTypes.string.isRequired,
|
folder: PropTypes.string.isRequired,
|
||||||
tags: PropTypes.object.isRequired,
|
tags: PropTypes.object.isRequired,
|
||||||
showLanguageProfile: PropTypes.bool.isRequired,
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ class AddNewSeriesModalContentConnector extends Component {
|
|||||||
this.props.setAddSeriesDefault({ [name]: value });
|
this.props.setAddSeriesDefault({ [name]: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddSeriesPress = (searchForMissingEpisodes, seriesType) => {
|
onAddSeriesPress = (seriesType) => {
|
||||||
const {
|
const {
|
||||||
tvdbId,
|
tvdbId,
|
||||||
rootFolderPath,
|
rootFolderPath,
|
||||||
@@ -63,6 +63,8 @@ class AddNewSeriesModalContentConnector extends Component {
|
|||||||
qualityProfileId,
|
qualityProfileId,
|
||||||
languageProfileId,
|
languageProfileId,
|
||||||
seasonFolder,
|
seasonFolder,
|
||||||
|
searchForMissingEpisodes,
|
||||||
|
searchForCutoffUnmetEpisodes,
|
||||||
tags
|
tags
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -74,8 +76,9 @@ class AddNewSeriesModalContentConnector extends Component {
|
|||||||
languageProfileId: languageProfileId.value,
|
languageProfileId: languageProfileId.value,
|
||||||
seriesType,
|
seriesType,
|
||||||
seasonFolder: seasonFolder.value,
|
seasonFolder: seasonFolder.value,
|
||||||
tags: tags.value,
|
searchForMissingEpisodes: searchForMissingEpisodes.value,
|
||||||
searchForMissingEpisodes
|
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
|
||||||
|
tags: tags.value
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +104,8 @@ AddNewSeriesModalContentConnector.propTypes = {
|
|||||||
languageProfileId: PropTypes.object,
|
languageProfileId: PropTypes.object,
|
||||||
seriesType: PropTypes.object.isRequired,
|
seriesType: PropTypes.object.isRequired,
|
||||||
seasonFolder: PropTypes.object.isRequired,
|
seasonFolder: PropTypes.object.isRequired,
|
||||||
|
searchForMissingEpisodes: PropTypes.object.isRequired,
|
||||||
|
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
|
||||||
tags: PropTypes.object.isRequired,
|
tags: PropTypes.object.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
setAddSeriesDefault: PropTypes.func.isRequired,
|
setAddSeriesDefault: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -34,10 +34,20 @@
|
|||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 0 1 100%;
|
flex: 0 1 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.titleContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex: 0 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
}
|
}
|
||||||
@@ -47,6 +57,14 @@
|
|||||||
color: $disabledColor;
|
color: $disabledColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icons {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
.tvdbLink {
|
.tvdbLink {
|
||||||
composes: link from '~Components/Link/Link.css';
|
composes: link from '~Components/Link/Link.css';
|
||||||
|
|
||||||
@@ -68,3 +86,10 @@
|
|||||||
.overview {
|
.overview {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointMedium) {
|
||||||
|
.titleRow {
|
||||||
|
justify-content: space-between;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -97,39 +97,45 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.title}>
|
<div className={styles.titleRow}>
|
||||||
{title}
|
<div className={styles.titleContainer}>
|
||||||
|
<div className={styles.title}>
|
||||||
|
{title}
|
||||||
|
|
||||||
{
|
{
|
||||||
!title.contains(year) && year ?
|
!title.contains(year) && year ?
|
||||||
<span className={styles.year}>
|
<span className={styles.year}>
|
||||||
({year})
|
({year})
|
||||||
</span> :
|
</span> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
<div className={styles.icons}>
|
||||||
isExistingSeries ?
|
{
|
||||||
|
isExistingSeries ?
|
||||||
|
<Icon
|
||||||
|
className={styles.alreadyExistsIcon}
|
||||||
|
name={icons.CHECK_CIRCLE}
|
||||||
|
size={36}
|
||||||
|
title="Already in your library"
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className={styles.tvdbLink}
|
||||||
|
to={`http://www.thetvdb.com/?tab=series&id=${tvdbId}`}
|
||||||
|
onPress={this.onTVDBLinkPress}
|
||||||
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.alreadyExistsIcon}
|
className={styles.tvdbLinkIcon}
|
||||||
name={icons.CHECK_CIRCLE}
|
name={icons.EXTERNAL_LINK}
|
||||||
size={36}
|
size={28}
|
||||||
title="Already in your library"
|
/>
|
||||||
/> :
|
</Link>
|
||||||
null
|
</div>
|
||||||
}
|
|
||||||
|
|
||||||
<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>
|
||||||
@@ -162,7 +168,7 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
Ended
|
Ended
|
||||||
</Label> :
|
</Label> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -173,7 +179,7 @@ class AddNewSeriesSearchResult extends Component {
|
|||||||
kind={kinds.INFO}
|
kind={kinds.INFO}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
Upcoming
|
Upcoming
|
||||||
</Label> :
|
</Label> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,24 +102,32 @@ class ImportSeries extends Component {
|
|||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
rootFoldersFetching && !rootFoldersPopulated &&
|
rootFoldersFetching ? <LoadingIndicator /> : null
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!rootFoldersFetching && !!rootFoldersError &&
|
!rootFoldersFetching && !!rootFoldersError ?
|
||||||
<div>Unable to load root folders</div>
|
<div>Unable to load root folders</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!rootFoldersError && rootFoldersPopulated && !unmappedFolders.length &&
|
!rootFoldersError &&
|
||||||
|
!rootFoldersFetching &&
|
||||||
|
rootFoldersPopulated &&
|
||||||
|
!unmappedFolders.length ?
|
||||||
<div>
|
<div>
|
||||||
All series in {path} have been imported
|
All series in {path} have been imported
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
|
!rootFoldersError &&
|
||||||
|
!rootFoldersFetching &&
|
||||||
|
rootFoldersPopulated &&
|
||||||
|
!!unmappedFolders.length &&
|
||||||
|
scroller ?
|
||||||
<ImportSeriesTableConnector
|
<ImportSeriesTableConnector
|
||||||
rootFolderId={rootFolderId}
|
rootFolderId={rootFolderId}
|
||||||
unmappedFolders={unmappedFolders}
|
unmappedFolders={unmappedFolders}
|
||||||
@@ -131,18 +139,22 @@ class ImportSeries extends Component {
|
|||||||
onSelectAllChange={this.onSelectAllChange}
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
|
|
||||||
{
|
{
|
||||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
!rootFoldersError &&
|
||||||
|
!rootFoldersFetching &&
|
||||||
|
!!unmappedFolders.length ?
|
||||||
<ImportSeriesFooterConnector
|
<ImportSeriesFooterConnector
|
||||||
selectedIds={this.getSelectedIds()}
|
selectedIds={this.getSelectedIds()}
|
||||||
showLanguageProfile={showLanguageProfile}
|
showLanguageProfile={showLanguageProfile}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onImportPress={this.onImportPress}
|
onImportPress={this.onImportPress}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class ImportSeriesConnector extends Component {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
|
rootFolderId,
|
||||||
qualityProfiles,
|
qualityProfiles,
|
||||||
languageProfiles,
|
languageProfiles,
|
||||||
defaultQualityProfileId,
|
defaultQualityProfileId,
|
||||||
@@ -84,9 +85,7 @@ class ImportSeriesConnector extends Component {
|
|||||||
dispatchSetAddSeriesDefault
|
dispatchSetAddSeriesDefault
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (!this.props.rootFoldersPopulated) {
|
dispatchFetchRootFolders({ id: rootFolderId, timeout: false });
|
||||||
dispatchFetchRootFolders();
|
|
||||||
}
|
|
||||||
|
|
||||||
let setDefaults = false;
|
let setDefaults = false;
|
||||||
const setDefaultPayload = {};
|
const setDefaultPayload = {};
|
||||||
@@ -154,6 +153,8 @@ const routeMatchShape = createRouteMatchShape({
|
|||||||
|
|
||||||
ImportSeriesConnector.propTypes = {
|
ImportSeriesConnector.propTypes = {
|
||||||
match: routeMatchShape.isRequired,
|
match: routeMatchShape.isRequired,
|
||||||
|
rootFolderId: PropTypes.number.isRequired,
|
||||||
|
rootFoldersFetching: PropTypes.bool.isRequired,
|
||||||
rootFoldersPopulated: PropTypes.bool.isRequired,
|
rootFoldersPopulated: PropTypes.bool.isRequired,
|
||||||
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
qualityProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
languageProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
languageProfiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -31,3 +31,7 @@
|
|||||||
margin: 0 10px 0 12px;
|
margin: 0 10px 0 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.importError {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import styles from './ImportSeriesFooter.css';
|
import styles from './ImportSeriesFooter.css';
|
||||||
|
|
||||||
const MIXED = 'mixed';
|
const MIXED = 'mixed';
|
||||||
@@ -118,6 +120,7 @@ class ImportSeriesFooter extends Component {
|
|||||||
isSeriesTypeMixed,
|
isSeriesTypeMixed,
|
||||||
hasUnsearchedItems,
|
hasUnsearchedItems,
|
||||||
showLanguageProfile,
|
showLanguageProfile,
|
||||||
|
importError,
|
||||||
onImportPress,
|
onImportPress,
|
||||||
onLookupPress,
|
onLookupPress,
|
||||||
onCancelLookupPress
|
onCancelLookupPress
|
||||||
@@ -226,38 +229,71 @@ class ImportSeriesFooter extends Component {
|
|||||||
</SpinnerButton>
|
</SpinnerButton>
|
||||||
|
|
||||||
{
|
{
|
||||||
isLookingUpSeries &&
|
isLookingUpSeries ?
|
||||||
<Button
|
<Button
|
||||||
className={styles.loadingButton}
|
className={styles.loadingButton}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
onPress={onCancelLookupPress}
|
onPress={onCancelLookupPress}
|
||||||
>
|
>
|
||||||
Cancel Processing
|
Cancel Processing
|
||||||
</Button>
|
</Button> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
hasUnsearchedItems &&
|
hasUnsearchedItems ?
|
||||||
<Button
|
<Button
|
||||||
className={styles.loadingButton}
|
className={styles.loadingButton}
|
||||||
kind={kinds.SUCCESS}
|
kind={kinds.SUCCESS}
|
||||||
onPress={onLookupPress}
|
onPress={onLookupPress}
|
||||||
>
|
>
|
||||||
Start Processing
|
Start Processing
|
||||||
</Button>
|
</Button> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isLookingUpSeries &&
|
isLookingUpSeries ?
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
className={styles.loading}
|
className={styles.loading}
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isLookingUpSeries &&
|
isLookingUpSeries ?
|
||||||
'Processing Folders'
|
'Processing Folders' :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
importError ?
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.importError}
|
||||||
|
name={icons.WARNING}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Import Errors"
|
||||||
|
body={
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
importError.responseJSON.map((error, index) => {
|
||||||
|
return (
|
||||||
|
<li key={index}>
|
||||||
|
{error.errorMessage}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -282,6 +318,7 @@ ImportSeriesFooter.propTypes = {
|
|||||||
isSeasonFolderMixed: PropTypes.bool.isRequired,
|
isSeasonFolderMixed: PropTypes.bool.isRequired,
|
||||||
hasUnsearchedItems: PropTypes.bool.isRequired,
|
hasUnsearchedItems: PropTypes.bool.isRequired,
|
||||||
showLanguageProfile: PropTypes.bool.isRequired,
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
|
importError: PropTypes.object,
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onImportPress: PropTypes.func.isRequired,
|
onImportPress: PropTypes.func.isRequired,
|
||||||
onLookupPress: PropTypes.func.isRequired,
|
onLookupPress: PropTypes.func.isRequired,
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ function createMapStateToProps() {
|
|||||||
const {
|
const {
|
||||||
isLookingUpSeries,
|
isLookingUpSeries,
|
||||||
isImporting,
|
isImporting,
|
||||||
items
|
items,
|
||||||
|
importError
|
||||||
} = importSeries;
|
} = importSeries;
|
||||||
|
|
||||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||||
@@ -51,6 +52,7 @@ function createMapStateToProps() {
|
|||||||
isLanguageProfileIdMixed,
|
isLanguageProfileIdMixed,
|
||||||
isSeriesTypeMixed,
|
isSeriesTypeMixed,
|
||||||
isSeasonFolderMixed,
|
isSeasonFolderMixed,
|
||||||
|
importError,
|
||||||
hasUnsearchedItems
|
hasUnsearchedItems
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ class ImportSeriesSelectSeries extends Component {
|
|||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
No match found!
|
No match found!
|
||||||
</div> :
|
</div> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ class ImportSeriesSelectSeries extends Component {
|
|||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
Search failed, please try again later.
|
Search failed, please try again later.
|
||||||
</div> :
|
</div> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ class ImportSeriesSelectFolder extends Component {
|
|||||||
Make sure that your files include the quality in their filenames. 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> Additionally, each series must be in its own folder within the root/library folder.
|
||||||
|
</li>
|
||||||
|
<li className={styles.tip}>
|
||||||
|
Do not use for importing downloads from your download client, this is only for existing organized libraries, not unsorted files.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementCo
|
|||||||
import Profiles from 'Settings/Profiles/Profiles';
|
import Profiles from 'Settings/Profiles/Profiles';
|
||||||
import Quality from 'Settings/Quality/Quality';
|
import Quality from 'Settings/Quality/Quality';
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||||
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||||
@@ -33,7 +34,6 @@ 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 {
|
||||||
@@ -171,6 +171,11 @@ function AppRoutes(props) {
|
|||||||
component={DownloadClientSettingsConnector}
|
component={DownloadClientSettingsConnector}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/importlists"
|
||||||
|
component={ImportListSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path="/settings/connect"
|
path="/settings/connect"
|
||||||
component={NotificationSettings}
|
component={NotificationSettings}
|
||||||
@@ -230,15 +235,6 @@ function AppRoutes(props) {
|
|||||||
component={Logs}
|
component={Logs}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/*
|
|
||||||
Diagnostics
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/diag"
|
|
||||||
component={Diagnostic}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
Not Found
|
Not Found
|
||||||
*/}
|
*/}
|
||||||
|
|||||||
@@ -10,9 +10,47 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||||
import styles from './AppUpdatedModalContent.css';
|
import styles from './AppUpdatedModalContent.css';
|
||||||
|
|
||||||
|
function mergeUpdates(items, version, prevVersion) {
|
||||||
|
let installedIndex = items.findIndex((u) => u.version === version);
|
||||||
|
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
|
||||||
|
|
||||||
|
if (installedIndex === -1) {
|
||||||
|
installedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installedPreviouslyIndex === -1) {
|
||||||
|
installedPreviouslyIndex = items.length;
|
||||||
|
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
||||||
|
installedPreviouslyIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
||||||
|
|
||||||
|
if (!appliedUpdates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedChanges = { new: [], fixed: [] };
|
||||||
|
appliedUpdates.forEach((u) => {
|
||||||
|
if (u.changes) {
|
||||||
|
appliedChanges.new.push(... u.changes.new);
|
||||||
|
appliedChanges.fixed.push(... u.changes.fixed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
|
||||||
|
|
||||||
|
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||||
|
mergedUpdate.changes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
function AppUpdatedModalContent(props) {
|
function AppUpdatedModalContent(props) {
|
||||||
const {
|
const {
|
||||||
version,
|
version,
|
||||||
|
prevVersion,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
@@ -20,7 +58,7 @@ function AppUpdatedModalContent(props) {
|
|||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const update = items[0];
|
const update = mergeUpdates(items, version, prevVersion);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
@@ -30,7 +68,7 @@ function AppUpdatedModalContent(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
Version <span className={styles.version}>{version}</span> of Sonarr has been installed, in order to get the latest changes you'll need to reload Sonarr.
|
Sonarr has been updated to version <span className={styles.version}>{version}</span>, in order to get the latest changes you'll need to reload Sonarr.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -88,6 +126,7 @@ function AppUpdatedModalContent(props) {
|
|||||||
|
|
||||||
AppUpdatedModalContent.propTypes = {
|
AppUpdatedModalContent.propTypes = {
|
||||||
version: PropTypes.string.isRequired,
|
version: PropTypes.string.isRequired,
|
||||||
|
prevVersion: PropTypes.string,
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import AppUpdatedModalContent from './AppUpdatedModalContent';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.app.version,
|
(state) => state.app.version,
|
||||||
|
(state) => state.app.prevVersion,
|
||||||
(state) => state.system.updates,
|
(state) => state.system.updates,
|
||||||
(version, updates) => {
|
(version, prevVersion, updates) => {
|
||||||
const {
|
const {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
@@ -18,6 +19,7 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
version,
|
version,
|
||||||
|
prevVersion,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items
|
items
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function ConnectionLostModal(props) {
|
|||||||
>
|
>
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Connnection Lost
|
Connection Lost
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
flex: 0 0 120px;
|
flex: 0 0 125px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ class FileBrowserModalContent extends Component {
|
|||||||
className={styles.mappedDrivesWarning}
|
className={styles.mappedDrivesWarning}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://github.com/Sonarr/Sonarr/wiki/FAQ">FAQ</Link> for more information.
|
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/Sonarr_FAQ#Why_cant_Sonarr_see_my_files_on_a_remote_server">FAQ</Link> for more information.
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import isString from 'Utilities/String/isString';
|
import isString from 'Utilities/String/isString';
|
||||||
import { IN_LAST, IN_NEXT } from 'Helpers/Props/filterTypes';
|
import { IN_LAST, NOT_IN_LAST, IN_NEXT, NOT_IN_NEXT } from 'Helpers/Props/filterTypes';
|
||||||
import NumberInput from 'Components/Form/NumberInput';
|
import NumberInput from 'Components/Form/NumberInput';
|
||||||
import SelectInput from 'Components/Form/SelectInput';
|
import SelectInput from 'Components/Form/SelectInput';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
@@ -18,7 +18,12 @@ const timeOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function isInFilter(filterType) {
|
function isInFilter(filterType) {
|
||||||
return filterType === IN_LAST || filterType === IN_NEXT;
|
return (
|
||||||
|
filterType === IN_LAST ||
|
||||||
|
filterType === NOT_IN_LAST ||
|
||||||
|
filterType === IN_NEXT ||
|
||||||
|
filterType === NOT_IN_NEXT
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DateFilterBuilderRowValue extends Component {
|
class DateFilterBuilderRowValue extends Component {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
|||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||||
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
|
||||||
|
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
|
||||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||||
import styles from './FilterBuilderRow.css';
|
import styles from './FilterBuilderRow.css';
|
||||||
|
|
||||||
@@ -75,6 +76,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.SERIES_STATUS:
|
case filterBuilderValueTypes.SERIES_STATUS:
|
||||||
return SeriesStatusFilterBuilderRowValue;
|
return SeriesStatusFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.SERIES_TYPES:
|
||||||
|
return SeriesTypeFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.TAG:
|
case filterBuilderValueTypes.TAG:
|
||||||
return TagFilterBuilderRowValueConnector;
|
return TagFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.tag {
|
.tag {
|
||||||
height: 21px;
|
display: flex;
|
||||||
|
|
||||||
&.isLastTag {
|
&.isLastTag {
|
||||||
.or {
|
.or {
|
||||||
@@ -18,4 +18,5 @@
|
|||||||
.or {
|
.or {
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
color: $themeDarkColor;
|
color: $themeDarkColor;
|
||||||
|
line-height: 31px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import styles from './FilterBuilderRowValueTag.css';
|
|||||||
|
|
||||||
function FilterBuilderRowValueTag(props) {
|
function FilterBuilderRowValueTag(props) {
|
||||||
return (
|
return (
|
||||||
<span
|
<div
|
||||||
className={styles.tag}
|
className={styles.tag}
|
||||||
>
|
>
|
||||||
<TagInputTag
|
<TagInputTag
|
||||||
@@ -15,12 +15,13 @@ function FilterBuilderRowValueTag(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!props.isLastTag &&
|
props.isLastTag ?
|
||||||
<span className={styles.or}>
|
null :
|
||||||
|
<div className={styles.or}>
|
||||||
or
|
or
|
||||||
</span>
|
</div>
|
||||||
}
|
}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
|
const seriesTypeList = [
|
||||||
|
{ id: 'anime', name: 'Anime' },
|
||||||
|
{ id: 'daily', name: 'Daily' },
|
||||||
|
{ id: 'standard', name: 'Standard' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function SeriesTypeFilterBuilderRowValue(props) {
|
||||||
|
return (
|
||||||
|
<FilterBuilderRowValue
|
||||||
|
tagList={seriesTypeList}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SeriesTypeFilterBuilderRowValue;
|
||||||
@@ -24,7 +24,7 @@ function CustomFiltersModalContent(props) {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
Custom Filters
|
Custom Filters
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
@@ -58,7 +58,7 @@ function CustomFiltersModalContent(props) {
|
|||||||
<Button
|
<Button
|
||||||
onPress={onModalClose}
|
onPress={onModalClose}
|
||||||
>
|
>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -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 { fetchOptions, clearOptions } from 'Store/Actions/providerOptionActions';
|
import { fetchOptions, clearOptions, defaultState } 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.providerOptions,
|
(state) => state.providerOptions.devices || defaultState,
|
||||||
(value, devices) => {
|
(value, devices) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -51,7 +51,7 @@ class DeviceInputConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount = () => {
|
componentWillUnmount = () => {
|
||||||
this.props.dispatchClearOptions();
|
this.props.dispatchClearOptions({ section: 'devices' });
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -65,6 +65,7 @@ class DeviceInputConnector extends Component {
|
|||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
dispatchFetchOptions({
|
dispatchFetchOptions({
|
||||||
|
section: 'devices',
|
||||||
action: 'getDevices',
|
action: 'getDevices',
|
||||||
provider,
|
provider,
|
||||||
providerData
|
providerData
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.editableContainer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
composes: hasError from '~Components/Form/Input.css';
|
composes: hasError from '~Components/Form/Input.css';
|
||||||
}
|
}
|
||||||
@@ -22,6 +26,16 @@
|
|||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdownArrowContainerEditable {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding-right: 17px;
|
||||||
|
width: 30%;
|
||||||
|
height: 35px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdownArrowContainerDisabled {
|
.dropdownArrowContainerDisabled {
|
||||||
composes: dropdownArrowContainer;
|
composes: dropdownArrowContainer;
|
||||||
|
|
||||||
@@ -66,3 +80,26 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: $white;
|
background-color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 5px -5px 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileCloseButtonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
height: 40px;
|
||||||
|
border-bottom: 1px solid $borderColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileCloseButton {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $modalCloseButtonHoverColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,10 +10,12 @@ import { icons, sizes, scrollDirections } from 'Helpers/Props';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Portal from 'Components/Portal';
|
import Portal from 'Components/Portal';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Measure from 'Components/Measure';
|
import Measure from 'Components/Measure';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import TextInput from './TextInput';
|
||||||
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
|
||||||
import HintedSelectInputOption from './HintedSelectInputOption';
|
import HintedSelectInputOption from './HintedSelectInputOption';
|
||||||
import styles from './EnhancedSelectInput.css';
|
import styles from './EnhancedSelectInput.css';
|
||||||
@@ -58,11 +60,30 @@ function getSelectedIndex(props) {
|
|||||||
values
|
values
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return values.findIndex((v) => {
|
||||||
|
return value.size && v.key === value[0];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return values.findIndex((v) => {
|
return values.findIndex((v) => {
|
||||||
return v.key === value;
|
return v.key === value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSelectedItem(index, props) {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
values
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.includes(values[index].key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return values[index].key === value;
|
||||||
|
}
|
||||||
|
|
||||||
function getKey(selectedIndex, values) {
|
function getKey(selectedIndex, values) {
|
||||||
return values[selectedIndex].key;
|
return values[selectedIndex].key;
|
||||||
}
|
}
|
||||||
@@ -92,7 +113,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
this._scheduleUpdate();
|
this._scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.value !== this.props.value) {
|
if (!Array.isArray(this.props.value) && prevProps.value !== this.props.value) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedIndex: getSelectedIndex(this.props)
|
selectedIndex: getSelectedIndex(this.props)
|
||||||
});
|
});
|
||||||
@@ -134,7 +155,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
const button = document.getElementById(this._buttonId);
|
const button = document.getElementById(this._buttonId);
|
||||||
const options = document.getElementById(this._optionsId);
|
const options = document.getElementById(this._optionsId);
|
||||||
|
|
||||||
if (!button || this.state.isMobile) {
|
if (!button || !event.target.isConnected || this.state.isMobile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,11 +170,21 @@ class EnhancedSelectInput extends Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onFocus = () => {
|
||||||
|
if (this.state.isOpen) {
|
||||||
|
this._removeListener();
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onBlur = () => {
|
onBlur = () => {
|
||||||
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
if (!this.props.isEditable) {
|
||||||
const origIndex = getSelectedIndex(this.props);
|
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
|
||||||
if (origIndex !== this.state.selectedIndex) {
|
const origIndex = getSelectedIndex(this.props);
|
||||||
this.setState({ selectedIndex: origIndex });
|
|
||||||
|
if (origIndex !== this.state.selectedIndex) {
|
||||||
|
this.setState({ selectedIndex: origIndex });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +208,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
selectedIndex == null ||
|
selectedIndex == null || selectedIndex === -1 ||
|
||||||
getSelectedOption(selectedIndex, values).isDisabled
|
getSelectedOption(selectedIndex, values).isDisabled
|
||||||
) {
|
) {
|
||||||
if (keyCode === keyCodes.UP_ARROW) {
|
if (keyCode === keyCodes.UP_ARROW) {
|
||||||
@@ -231,16 +262,35 @@ class EnhancedSelectInput extends Component {
|
|||||||
this._addListener();
|
this._addListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.state.isOpen && this.props.onOpen) {
|
||||||
|
this.props.onOpen();
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ isOpen: !this.state.isOpen });
|
this.setState({ isOpen: !this.state.isOpen });
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelect = (value) => {
|
onSelect = (value) => {
|
||||||
this.setState({ isOpen: false });
|
if (Array.isArray(this.props.value)) {
|
||||||
|
let newValue = null;
|
||||||
|
const index = this.props.value.indexOf(value);
|
||||||
|
if (index === -1) {
|
||||||
|
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
||||||
|
} else {
|
||||||
|
newValue = [...this.props.value];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
}
|
||||||
|
this.props.onChange({
|
||||||
|
name: this.props.name,
|
||||||
|
value: newValue
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
name: this.props.name,
|
name: this.props.name,
|
||||||
value
|
value
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
onMeasure = ({ width }) => {
|
||||||
@@ -258,14 +308,19 @@ class EnhancedSelectInput extends Component {
|
|||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
disabledClassName,
|
disabledClassName,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
values,
|
values,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
isEditable,
|
||||||
|
isFetching,
|
||||||
hasError,
|
hasError,
|
||||||
hasWarning,
|
hasWarning,
|
||||||
valueOptions,
|
valueOptions,
|
||||||
selectedValueOptions,
|
selectedValueOptions,
|
||||||
selectedValueComponent: SelectedValueComponent,
|
selectedValueComponent: SelectedValueComponent,
|
||||||
optionComponent: OptionComponent
|
optionComponent: OptionComponent,
|
||||||
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -275,6 +330,7 @@ class EnhancedSelectInput extends Component {
|
|||||||
isMobile
|
isMobile
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
|
const isMultiSelect = Array.isArray(value);
|
||||||
const selectedOption = getSelectedOption(selectedIndex, values);
|
const selectedOption = getSelectedOption(selectedIndex, values);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -290,37 +346,94 @@ class EnhancedSelectInput extends Component {
|
|||||||
whitelist={['width']}
|
whitelist={['width']}
|
||||||
onMeasure={this.onMeasure}
|
onMeasure={this.onMeasure}
|
||||||
>
|
>
|
||||||
<Link
|
{
|
||||||
className={classNames(
|
isEditable ?
|
||||||
className,
|
<div
|
||||||
hasError && styles.hasError,
|
className={styles.editableContainer}
|
||||||
hasWarning && styles.hasWarning,
|
>
|
||||||
isDisabled && disabledClassName
|
<TextInput
|
||||||
)}
|
className={className}
|
||||||
isDisabled={isDisabled}
|
name={name}
|
||||||
onBlur={this.onBlur}
|
value={value}
|
||||||
onKeyDown={this.onKeyDown}
|
readOnly={isDisabled}
|
||||||
onPress={this.onPress}
|
hasError={hasError}
|
||||||
>
|
hasWarning={hasWarning}
|
||||||
<SelectedValueComponent
|
onFocus={this.onFocus}
|
||||||
{...selectedValueOptions}
|
onBlur={this.onBlur}
|
||||||
{...selectedOption}
|
onChange={onChange}
|
||||||
isDisabled={isDisabled}
|
/>
|
||||||
>
|
<Link
|
||||||
{selectedOption ? selectedOption.value : null}
|
className={classNames(
|
||||||
</SelectedValueComponent>
|
styles.dropdownArrowContainerEditable,
|
||||||
|
isDisabled ?
|
||||||
|
styles.dropdownArrowContainerDisabled :
|
||||||
|
styles.dropdownArrowContainer)
|
||||||
|
}
|
||||||
|
onPress={this.onPress}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
<div
|
{
|
||||||
className={isDisabled ?
|
!isFetching &&
|
||||||
styles.dropdownArrowContainerDisabled :
|
<Icon
|
||||||
styles.dropdownArrowContainer
|
name={icons.CARET_DOWN}
|
||||||
}
|
/>
|
||||||
>
|
}
|
||||||
<Icon
|
</Link>
|
||||||
name={icons.CARET_DOWN}
|
</div> :
|
||||||
/>
|
<Link
|
||||||
</div>
|
className={classNames(
|
||||||
</Link>
|
className,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning,
|
||||||
|
isDisabled && disabledClassName
|
||||||
|
)}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
onKeyDown={this.onKeyDown}
|
||||||
|
onPress={this.onPress}
|
||||||
|
>
|
||||||
|
<SelectedValueComponent
|
||||||
|
value={value}
|
||||||
|
values={values}
|
||||||
|
{...selectedValueOptions}
|
||||||
|
{...selectedOption}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
|
>
|
||||||
|
{selectedOption ? selectedOption.value : null}
|
||||||
|
</SelectedValueComponent>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={isDisabled ?
|
||||||
|
styles.dropdownArrowContainerDisabled :
|
||||||
|
styles.dropdownArrowContainer
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator
|
||||||
|
className={styles.loading}
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching &&
|
||||||
|
<Icon
|
||||||
|
name={icons.CARET_DOWN}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
</Measure>
|
</Measure>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -359,11 +472,17 @@ class EnhancedSelectInput extends Component {
|
|||||||
>
|
>
|
||||||
{
|
{
|
||||||
values.map((v, index) => {
|
values.map((v, index) => {
|
||||||
|
const hasParent = v.parentKey !== undefined;
|
||||||
|
const depth = hasParent ? 1 : 0;
|
||||||
|
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||||
return (
|
return (
|
||||||
<OptionComponent
|
<OptionComponent
|
||||||
key={v.key}
|
key={v.key}
|
||||||
id={v.key}
|
id={v.key}
|
||||||
isSelected={index === selectedIndex}
|
depth={depth}
|
||||||
|
isSelected={isSelectedItem(index, this.props)}
|
||||||
|
isDisabled={parentSelected}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
{...valueOptions}
|
{...valueOptions}
|
||||||
{...v}
|
{...v}
|
||||||
isMobile={false}
|
isMobile={false}
|
||||||
@@ -399,13 +518,31 @@ class EnhancedSelectInput extends Component {
|
|||||||
scrollDirection={scrollDirections.NONE}
|
scrollDirection={scrollDirections.NONE}
|
||||||
>
|
>
|
||||||
<Scroller className={styles.optionsModalScroller}>
|
<Scroller className={styles.optionsModalScroller}>
|
||||||
|
<div className={styles.mobileCloseButtonContainer}>
|
||||||
|
<Link
|
||||||
|
className={styles.mobileCloseButton}
|
||||||
|
onPress={this.onOptionsModalClose}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={icons.CLOSE}
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
values.map((v, index) => {
|
values.map((v, index) => {
|
||||||
|
const hasParent = v.parentKey !== undefined;
|
||||||
|
const depth = hasParent ? 1 : 0;
|
||||||
|
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||||
return (
|
return (
|
||||||
<OptionComponent
|
<OptionComponent
|
||||||
key={v.key}
|
key={v.key}
|
||||||
id={v.key}
|
id={v.key}
|
||||||
isSelected={index === selectedIndex}
|
depth={depth}
|
||||||
|
isSelected={isSelectedItem(index, this.props)}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
|
isDisabled={parentSelected}
|
||||||
{...valueOptions}
|
{...valueOptions}
|
||||||
{...v}
|
{...v}
|
||||||
isMobile={true}
|
isMobile={true}
|
||||||
@@ -429,15 +566,18 @@ EnhancedSelectInput.propTypes = {
|
|||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
disabledClassName: PropTypes.string,
|
disabledClassName: PropTypes.string,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isEditable: PropTypes.bool.isRequired,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
hasWarning: PropTypes.bool,
|
hasWarning: PropTypes.bool,
|
||||||
valueOptions: PropTypes.object.isRequired,
|
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.elementType,
|
optionComponent: PropTypes.elementType,
|
||||||
|
onOpen: PropTypes.func,
|
||||||
onChange: PropTypes.func.isRequired
|
onChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -445,6 +585,8 @@ EnhancedSelectInput.defaultProps = {
|
|||||||
className: styles.enhancedSelect,
|
className: styles.enhancedSelect,
|
||||||
disabledClassName: styles.isDisabled,
|
disabledClassName: styles.isDisabled,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
|
isFetching: false,
|
||||||
|
isEditable: false,
|
||||||
valueOptions: {},
|
valueOptions: {},
|
||||||
selectedValueOptions: {},
|
selectedValueOptions: {},
|
||||||
selectedValueComponent: HintedSelectInputSelectedValue,
|
selectedValueComponent: HintedSelectInputSelectedValue,
|
||||||
|
|||||||
159
frontend/src/Components/Form/EnhancedSelectInputConnector.js
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { fetchOptions, clearOptions, defaultState } from 'Store/Actions/providerOptionActions';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
const importantFieldNames = [
|
||||||
|
'baseUrl',
|
||||||
|
'apiPath',
|
||||||
|
'apiKey'
|
||||||
|
];
|
||||||
|
|
||||||
|
function getProviderDataKey(providerData) {
|
||||||
|
if (!providerData || !providerData.fields) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields = providerData.fields
|
||||||
|
.filter((f) => importantFieldNames.includes(f.name))
|
||||||
|
.map((f) => f.value);
|
||||||
|
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSelectOptions(items) {
|
||||||
|
if (!items) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.map((option) => {
|
||||||
|
return {
|
||||||
|
key: option.value,
|
||||||
|
value: option.name,
|
||||||
|
hint: option.hint,
|
||||||
|
parentKey: option.parentValue
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { selectOptionsProviderAction }) => state.providerOptions[selectOptionsProviderAction] || defaultState,
|
||||||
|
(options) => {
|
||||||
|
if (options) {
|
||||||
|
return {
|
||||||
|
isFetching: options.isFetching,
|
||||||
|
values: getSelectOptions(options.items)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
dispatchFetchOptions: fetchOptions,
|
||||||
|
dispatchClearOptions: clearOptions
|
||||||
|
};
|
||||||
|
|
||||||
|
class EnhancedSelectInputConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
refetchRequired: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount = () => {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate = (prevProps) => {
|
||||||
|
const prevKey = getProviderDataKey(prevProps.providerData);
|
||||||
|
const nextKey = getProviderDataKey(this.props.providerData);
|
||||||
|
|
||||||
|
if (!_.isEqual(prevKey, nextKey)) {
|
||||||
|
this.setState({ refetchRequired: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount = () => {
|
||||||
|
this._cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onOpen = () => {
|
||||||
|
if (this.state.refetchRequired) {
|
||||||
|
this._populate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
_populate() {
|
||||||
|
const {
|
||||||
|
provider,
|
||||||
|
providerData,
|
||||||
|
selectOptionsProviderAction,
|
||||||
|
dispatchFetchOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
this.setState({ refetchRequired: false });
|
||||||
|
dispatchFetchOptions({
|
||||||
|
section: selectOptionsProviderAction,
|
||||||
|
action: selectOptionsProviderAction,
|
||||||
|
provider,
|
||||||
|
providerData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_cleanup() {
|
||||||
|
const {
|
||||||
|
selectOptionsProviderAction,
|
||||||
|
dispatchClearOptions
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (selectOptionsProviderAction) {
|
||||||
|
dispatchClearOptions({ section: selectOptionsProviderAction });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...this.props}
|
||||||
|
onOpen={this.onOpen}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EnhancedSelectInputConnector.propTypes = {
|
||||||
|
provider: PropTypes.string.isRequired,
|
||||||
|
providerData: PropTypes.object.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||||
|
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectOptionsProviderAction: PropTypes.string,
|
||||||
|
onChange: PropTypes.func.isRequired,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
dispatchFetchOptions: PropTypes.func.isRequired,
|
||||||
|
dispatchClearOptions: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(EnhancedSelectInputConnector);
|
||||||
@@ -11,6 +11,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.optionCheck {
|
||||||
|
composes: container from '~./CheckInput.css';
|
||||||
|
|
||||||
|
flex: 0 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.optionCheckInput {
|
||||||
|
composes: input from '~./CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.isSelected {
|
.isSelected {
|
||||||
background-color: #e2e2e2;
|
background-color: #e2e2e2;
|
||||||
|
|
||||||
@@ -42,4 +54,8 @@
|
|||||||
&:last-child {
|
&:last-child {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import classNames from 'classnames';
|
|||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import CheckInput from './CheckInput';
|
||||||
import styles from './EnhancedSelectInputOption.css';
|
import styles from './EnhancedSelectInputOption.css';
|
||||||
|
|
||||||
class EnhancedSelectInputOption extends Component {
|
class EnhancedSelectInputOption extends Component {
|
||||||
@@ -11,7 +12,9 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onPress = () => {
|
onPress = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
onSelect
|
onSelect
|
||||||
@@ -20,15 +23,22 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
onSelect(id);
|
onSelect(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCheckPress = () => {
|
||||||
|
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
|
id,
|
||||||
|
depth,
|
||||||
isSelected,
|
isSelected,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
isHidden,
|
isHidden,
|
||||||
|
isMultiSelect,
|
||||||
isMobile,
|
isMobile,
|
||||||
children
|
children
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -37,8 +47,8 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
<Link
|
<Link
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
isSelected && styles.isSelected,
|
isSelected && !isMultiSelect && styles.isSelected,
|
||||||
isDisabled && styles.isDisabled,
|
isDisabled && !isMultiSelect && styles.isDisabled,
|
||||||
isHidden && styles.isHidden,
|
isHidden && styles.isHidden,
|
||||||
isMobile && styles.isMobile
|
isMobile && styles.isMobile
|
||||||
)}
|
)}
|
||||||
@@ -46,6 +56,24 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
depth !== 0 &&
|
||||||
|
<div style={{ width: `${depth * 20}px` }} />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isMultiSelect &&
|
||||||
|
<CheckInput
|
||||||
|
className={styles.optionCheckInput}
|
||||||
|
containerClassName={styles.optionCheck}
|
||||||
|
name={`select-${id}`}
|
||||||
|
value={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
onChange={this.onCheckPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -64,9 +92,11 @@ class EnhancedSelectInputOption extends Component {
|
|||||||
EnhancedSelectInputOption.propTypes = {
|
EnhancedSelectInputOption.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
depth: 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,
|
||||||
|
isMultiSelect: PropTypes.bool.isRequired,
|
||||||
isMobile: PropTypes.bool.isRequired,
|
isMobile: PropTypes.bool.isRequired,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
onSelect: PropTypes.func.isRequired
|
onSelect: PropTypes.func.isRequired
|
||||||
@@ -74,8 +104,10 @@ EnhancedSelectInputOption.propTypes = {
|
|||||||
|
|
||||||
EnhancedSelectInputOption.defaultProps = {
|
EnhancedSelectInputOption.defaultProps = {
|
||||||
className: styles.option,
|
className: styles.option,
|
||||||
|
depth: 0,
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isHidden: false
|
isHidden: false,
|
||||||
|
isMultiSelect: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EnhancedSelectInputOption;
|
export default EnhancedSelectInputOption;
|
||||||
|
|||||||
@@ -18,9 +18,12 @@ 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';
|
||||||
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import TagInputConnector from './TagInputConnector';
|
import TagInputConnector from './TagInputConnector';
|
||||||
|
import TagSelectInputConnector from './TagSelectInputConnector';
|
||||||
import TextTagInputConnector from './TextTagInputConnector';
|
import TextTagInputConnector from './TextTagInputConnector';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
import UMaskInput from './UMaskInput';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import styles from './FormInputGroup.css';
|
import styles from './FormInputGroup.css';
|
||||||
|
|
||||||
@@ -71,6 +74,9 @@ function getComponent(type) {
|
|||||||
case inputTypes.SELECT:
|
case inputTypes.SELECT:
|
||||||
return EnhancedSelectInput;
|
return EnhancedSelectInput;
|
||||||
|
|
||||||
|
case inputTypes.DYNAMIC_SELECT:
|
||||||
|
return EnhancedSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.SERIES_TYPE_SELECT:
|
case inputTypes.SERIES_TYPE_SELECT:
|
||||||
return SeriesTypeSelectInput;
|
return SeriesTypeSelectInput;
|
||||||
|
|
||||||
@@ -80,6 +86,12 @@ function getComponent(type) {
|
|||||||
case inputTypes.TEXT_TAG:
|
case inputTypes.TEXT_TAG:
|
||||||
return TextTagInputConnector;
|
return TextTagInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.TAG_SELECT:
|
||||||
|
return TagSelectInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.UMASK:
|
||||||
|
return UMaskInput;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return TextInput;
|
return TextInput;
|
||||||
}
|
}
|
||||||
@@ -187,7 +199,7 @@ function FormInputGroup(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!checkInput && helpTextWarning &&
|
(!checkInput || helpText) && helpTextWarning &&
|
||||||
<FormInputHelpText
|
<FormInputHelpText
|
||||||
text={helpTextWarning}
|
text={helpTextWarning}
|
||||||
isWarning={true}
|
isWarning={true}
|
||||||
@@ -210,7 +222,7 @@ function FormInputGroup(props) {
|
|||||||
key={index}
|
key={index}
|
||||||
text={error.message}
|
text={error.message}
|
||||||
link={error.link}
|
link={error.link}
|
||||||
linkTooltip={error.detailedMessage}
|
tooltip={error.detailedMessage}
|
||||||
isError={true}
|
isError={true}
|
||||||
isCheckInput={checkInput}
|
isCheckInput={checkInput}
|
||||||
/>
|
/>
|
||||||
@@ -225,7 +237,7 @@ function FormInputGroup(props) {
|
|||||||
key={index}
|
key={index}
|
||||||
text={warning.message}
|
text={warning.message}
|
||||||
link={warning.link}
|
link={warning.link}
|
||||||
linkTooltip={warning.detailedMessage}
|
tooltip={warning.detailedMessage}
|
||||||
isWarning={true}
|
isWarning={true}
|
||||||
isCheckInput={checkInput}
|
isCheckInput={checkInput}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,3 +37,7 @@
|
|||||||
|
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.details {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ function FormInputHelpText(props) {
|
|||||||
className,
|
className,
|
||||||
text,
|
text,
|
||||||
link,
|
link,
|
||||||
linkTooltip,
|
tooltip,
|
||||||
isError,
|
isError,
|
||||||
isWarning,
|
isWarning,
|
||||||
isCheckInput
|
isCheckInput
|
||||||
@@ -28,16 +28,27 @@ function FormInputHelpText(props) {
|
|||||||
{text}
|
{text}
|
||||||
|
|
||||||
{
|
{
|
||||||
!!link &&
|
link ?
|
||||||
<Link
|
<Link
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
to={link}
|
to={link}
|
||||||
title={linkTooltip}
|
title={tooltip}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.EXTERNAL_LINK}
|
name={icons.EXTERNAL_LINK}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!link && tooltip ?
|
||||||
|
<Icon
|
||||||
|
containerClassName={styles.details}
|
||||||
|
name={icons.INFO}
|
||||||
|
title={tooltip}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -47,7 +58,7 @@ FormInputHelpText.propTypes = {
|
|||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
text: PropTypes.string.isRequired,
|
text: PropTypes.string.isRequired,
|
||||||
link: PropTypes.string,
|
link: PropTypes.string,
|
||||||
linkTooltip: PropTypes.string,
|
tooltip: PropTypes.string,
|
||||||
isError: PropTypes.bool,
|
isError: PropTypes.bool,
|
||||||
isWarning: PropTypes.bool,
|
isWarning: PropTypes.bool,
|
||||||
isCheckInput: PropTypes.bool
|
isCheckInput: PropTypes.bool
|
||||||
|
|||||||
@@ -6,14 +6,25 @@ import styles from './HintedSelectInputOption.css';
|
|||||||
|
|
||||||
function HintedSelectInputOption(props) {
|
function HintedSelectInputOption(props) {
|
||||||
const {
|
const {
|
||||||
|
id,
|
||||||
value,
|
value,
|
||||||
hint,
|
hint,
|
||||||
|
depth,
|
||||||
|
isSelected,
|
||||||
|
isDisabled,
|
||||||
|
isMultiSelect,
|
||||||
isMobile,
|
isMobile,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInputOption
|
<EnhancedSelectInputOption
|
||||||
|
id={id}
|
||||||
|
depth={depth}
|
||||||
|
isSelected={isSelected}
|
||||||
|
isDisabled={isDisabled}
|
||||||
|
isHidden={isDisabled}
|
||||||
|
isMultiSelect={isMultiSelect}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
@@ -36,9 +47,20 @@ function HintedSelectInputOption(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
HintedSelectInputOption.propTypes = {
|
HintedSelectInputOption.propTypes = {
|
||||||
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
hint: PropTypes.node,
|
hint: PropTypes.node,
|
||||||
|
depth: PropTypes.number,
|
||||||
|
isSelected: PropTypes.bool.isRequired,
|
||||||
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
isMultiSelect: PropTypes.bool.isRequired,
|
||||||
isMobile: PropTypes.bool.isRequired
|
isMobile: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
HintedSelectInputOption.defaultProps = {
|
||||||
|
isDisabled: false,
|
||||||
|
isHidden: false,
|
||||||
|
isMultiSelect: false
|
||||||
|
};
|
||||||
|
|
||||||
export default HintedSelectInputOption;
|
export default HintedSelectInputOption;
|
||||||
|
|||||||