mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
279 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8da7aae03 | |||
| c165118d4d | |||
| b3dd571a92 | |||
| dd900eb739 | |||
| 66aae0c91c | |||
| d888a0a2b3 | |||
| cb5416a18c | |||
| 7977e0be05 | |||
| cd836fef38 | |||
| b0bfbe767c | |||
| 528b93dabe | |||
| 1edcbee5e1 | |||
| 8853dced9f | |||
| c7aa1bae5e | |||
| 405ae77070 | |||
| 6236bc9b4f | |||
| 743c977e5b | |||
| c0e5646f07 | |||
| 10094b4e66 | |||
| d923406f08 | |||
| 69a9c72286 | |||
| 55b9477a01 | |||
| 6b81f92137 | |||
| 3ceda1bcda | |||
| f1f1921517 | |||
| af0c96538a | |||
| 3d52f45b6a | |||
| d4715f119d | |||
| d58135bf17 | |||
| b452c10da3 | |||
| f6b364725d | |||
| 99f6be3f3d | |||
| c2ac49a873 | |||
| 0e24a3e8bc | |||
| 18032cc83b | |||
| 927eb38945 | |||
| 5fac348613 | |||
| 7ba9603449 | |||
| e36de8ab8d | |||
| f8704a1655 | |||
| f507d5154e | |||
| 5f03e7142a | |||
| c0ebbee7c9 | |||
| 4051cf3d80 | |||
| 9876ed64e2 | |||
| 2f26974ecc | |||
| 25f66a3029 | |||
| 0e25b2708c | |||
| 410870d21e | |||
| a64d931904 | |||
| f0a9e76cfc | |||
| 6f23c465ee | |||
| af60cca9ae | |||
| d34d23a052 | |||
| 0a0da42543 | |||
| e5419f6f06 | |||
| 88d9c08f1a | |||
| 6b4259757c | |||
| f1d7c56d94 | |||
| c81b2e80ee | |||
| 5efefd804b | |||
| 38f9543526 | |||
| aae68e681e | |||
| 1d21bbf78f | |||
| 99c3c8ce5b | |||
| 85171e40a5 | |||
| 86b656d323 | |||
| 5ae5d1043a | |||
| b801aa0935 | |||
| b2b5aa1f79 | |||
| 8c6ba9a543 | |||
| 4e024c51d3 | |||
| e4106f0ede | |||
| 9032ac20ff | |||
| 23fce4bf2e | |||
| 64fd8552f8 | |||
| e016410c10 | |||
| bea943adf8 | |||
| 9780d20f8a | |||
| 62722d45b0 | |||
| 27dd8e8cd5 | |||
| 6c47ede76b | |||
| 7b9562bb38 | |||
| 8b0b7c1cb0 | |||
| 7ebd341cd6 | |||
| 6c85f166ff | |||
| 45aabce107 | |||
| 0caa793df4 | |||
| 9a107cc8d7 | |||
| a6d727fe2a | |||
| 01a53d3624 | |||
| 348c29c9d7 | |||
| 64739712c6 | |||
| 6ac9cca953 | |||
| a2b38c5b7d | |||
| 3cc4105d71 | |||
| 3449a5d3fe | |||
| 5bac157d36 | |||
| 114d260f42 | |||
| 617b9c5d35 | |||
| ba4ccbb0bd | |||
| b845268b3d | |||
| 0fee552074 | |||
| 828b994ef4 | |||
| 7952fd325b | |||
| 4b4e598b67 | |||
| 71ccebd0f5 | |||
| 2607c67912 | |||
| a626b4f3c4 | |||
| 1526bf29f4 | |||
| 2194772736 | |||
| cd490d6334 | |||
| ff609848d8 | |||
| 15b6f7212d | |||
| af06a9f70d | |||
| c3fa440cf8 | |||
| 0411d66520 | |||
| 179637fe8b | |||
| 09b4bf15cf | |||
| ea86d14ca7 | |||
| 2429dd91c6 | |||
| a752476cdb | |||
| 50ce480abf | |||
| 0ef6e56e5d | |||
| 12d5014125 | |||
| c8301d425c | |||
| b1df9b2401 | |||
| ff09da3a69 | |||
| 3b9bd696fb | |||
| 9ab3e6bab7 | |||
| 86f4f86a0a | |||
| 40d95a04e3 | |||
| ca724836ce | |||
| 10e3964111 | |||
| b22a86e1d7 | |||
| 5976d66511 | |||
| b4eff4d4f9 | |||
| 1414a09111 | |||
| b30efd0c62 | |||
| def6950db4 | |||
| f23c2dbaba | |||
| 186e9cdd23 | |||
| 394f34eb2a | |||
| d9f508280d | |||
| b5505800de | |||
| 48a79eb7d3 | |||
| b42f7e09f9 | |||
| 8f507ac726 | |||
| 06d54e0ec2 | |||
| 3708d58847 | |||
| 0049ccd39f | |||
| ab8a2d190e | |||
| 25bb52b206 | |||
| 63c6f70e67 | |||
| 79cd6269f4 | |||
| 879c872179 | |||
| d4993cf69b | |||
| 781e0c9d1c | |||
| c946ed83f9 | |||
| 9aecf94e8e | |||
| 234e23eb47 | |||
| 748d888520 | |||
| 00d50a030c | |||
| 017fa5ad80 | |||
| d5fb1c55c6 | |||
| 1be8385c41 | |||
| 6f26c55a1b | |||
| 2d2de7f76b | |||
| 2c9292c249 | |||
| 6747b74271 | |||
| 13f10906f1 | |||
| 25d08a67e4 | |||
| aadefbe3b0 | |||
| d99a7e9b8a | |||
| dc29526961 | |||
| f8e47fbdc7 | |||
| 56a7725e52 | |||
| abf1b9d6cf | |||
| dd90bf53dd | |||
| cd29c0c9c8 | |||
| 9986d04d36 | |||
| f900d623dc | |||
| 84b507faf3 | |||
| adb27123df | |||
| a06792b923 | |||
| d90ee3ae11 | |||
| ff38afd198 | |||
| db70c06b8b | |||
| fb7656be56 | |||
| 3287e7cdec | |||
| 0761e27cfa | |||
| 4f47bb39ac | |||
| 889d071004 | |||
| 0049922ab6 | |||
| 3c995a0fff | |||
| 430719baac | |||
| 9928d711a3 | |||
| f90b43b3e1 | |||
| 64122b4cfb | |||
| 7912a942f7 | |||
| 0a7607bb62 | |||
| beeb5204b8 | |||
| ab13fb6e99 | |||
| 2a3d595a66 | |||
| 958a863d8f | |||
| 8b7884deb0 | |||
| 9a22e1c791 | |||
| f0f828491b | |||
| 7f3d107eda | |||
| ce4477eeac | |||
| 8b64f873f4 | |||
| 38bd060960 | |||
| 7c243cb6e8 | |||
| b29dee63f4 | |||
| f6542bab0a | |||
| da1b53b7e2 | |||
| 0deae95782 | |||
| 75c7a3cfc6 | |||
| cfdb7a15de | |||
| 63a7d33e7e | |||
| c9836f997c | |||
| d37e71415f | |||
| 9a5f4bef63 | |||
| 40551ba5a3 | |||
| 6e04dc894b | |||
| ac767ed386 | |||
| 42fbb79017 | |||
| c43bd77dae | |||
| 68dfa55b35 | |||
| fa190c85a3 | |||
| 172dcf6f8d | |||
| 0736fc955f | |||
| 9d0b8d974d | |||
| 9a3e89f283 | |||
| e33e45ec73 | |||
| 5893d88058 | |||
| a81d27acda | |||
| d2b279a6be | |||
| 6686fa0600 | |||
| 1d286df85d | |||
| be2e1e4fdb | |||
| 08868e5d01 | |||
| 7b43c2e345 | |||
| dc599b6531 | |||
| 1421179654 | |||
| 41dcf32e24 | |||
| 7a813a44b6 | |||
| 54a5059080 | |||
| adaf7444d3 | |||
| 49d11e59b3 | |||
| a7eb4a4a04 | |||
| 66a6a663ba | |||
| f735e31835 | |||
| b8f1286abb | |||
| 9df45199d0 | |||
| a692c35b03 | |||
| ddcad270c3 | |||
| b06f1d7c12 | |||
| 480bb50b85 | |||
| dbc94dbe4e | |||
| b89271fc01 | |||
| 66fcde7325 | |||
| 463741da1f | |||
| 3388fae1a5 | |||
| 72b2cfe8be | |||
| d5dd5e08ca | |||
| fabd40cbae | |||
| 3ca327f611 | |||
| c804140896 | |||
| bb43d0c796 | |||
| 5757fa797f | |||
| 2fc32189d8 | |||
| 5975be3690 | |||
| 6095819005 | |||
| 7528882adf | |||
| c1f1307345 | |||
| 348060351a | |||
| ca31cdd33a | |||
| 36e278aa82 |
@@ -1,33 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,66 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB |
@@ -1,50 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
@@ -1,42 +0,0 @@
|
|||||||
<?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">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="linear-gradient" x1="70.22612" y1="27.79912" x2="-5.13024" y2="63.12242" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.22111" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.2356" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.35559" stop-color="#ca135c"/>
|
|
||||||
<stop offset="0.46633" stop-color="#ce1e57"/>
|
|
||||||
<stop offset="0.5735" stop-color="#d4314e"/>
|
|
||||||
<stop offset="0.67844" stop-color="#dc4b41"/>
|
|
||||||
<stop offset="0.78179" stop-color="#e66d31"/>
|
|
||||||
<stop offset="0.88253" stop-color="#f3961d"/>
|
|
||||||
<stop offset="0.94241" stop-color="#fcb20f"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-2" x1="24.65904" y1="61.99608" x2="46.04762" y2="2.93445" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0.04188" stop-color="#077cfb"/>
|
|
||||||
<stop offset="0.44503" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.95812" stop-color="#077cfb"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-3" x1="17.39552" y1="63.34592" x2="33.19389" y2="7.20092" gradientTransform="matrix(1, 0, 0, -1, 0, 71.27997)" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0.27749" stop-color="#c90f5e"/>
|
|
||||||
<stop offset="0.97382" stop-color="#fcb20f"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<title>rider</title>
|
|
||||||
<g>
|
|
||||||
<polygon points="70 27.237 63.391 23.75 20.926 0 3.827 17.921 21.619 41.068 60.537 44.397 70 27.237" fill="url(#linear-gradient)"/>
|
|
||||||
<polygon points="50.423 16.132 44.271 1.107 27.643 17.471 11.768 50.194 49.411 70 70 57.98 50.423 16.132" fill="url(#linear-gradient-2)"/>
|
|
||||||
<polygon points="20.926 0 0 14.095 7.779 62.172 27.848 69.889 53.78 48.823 20.926 0" fill="url(#linear-gradient-3)"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<rect x="13.30219" y="13.19311" width="43.61371" height="43.61371"/>
|
|
||||||
<g>
|
|
||||||
<path d="M17.22741,18.86293h8.39564a7.38416,7.38416,0,0,1,5.34268,1.85358,5.86989,5.86989,0,0,1,1.52648,4.1433h0A5.74339,5.74339,0,0,1,28.567,30.5296l4.47041,6.54206H28.34891L24.42368,31.1838h-3.162v5.88785H17.22741V18.86293h0ZM25.296,27.69471c1.96262,0,3.053-1.09034,3.053-2.61682h0c0-1.74455-1.19938-2.61682-3.162-2.61682H21.15265v5.23365H25.296Z" fill="#fff"/>
|
|
||||||
<path d="M36.09034,18.86293H43.2866c5.77882,0,9.70405,3.92523,9.70405,9.15888h0c0,5.12461-3.92523,9.15888-9.70405,9.15888H36.09034V18.86293Zm4.03427,3.59813V33.47352h3.162a5.23727,5.23727,0,0,0,5.56075-5.45171h0a5.26493,5.26493,0,0,0-5.56075-5.56075h-3.162Z" fill="#fff"/>
|
|
||||||
</g>
|
|
||||||
<rect x="17.22741" y="48.62925" width="16.35514" height="2.72586" fill="#fff"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.0 KiB |
@@ -1,36 +0,0 @@
|
|||||||
<?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="25.0676" y1="1.4599" x2="43.1829" y2="66.675">
|
|
||||||
<stop offset="0.2849" style="stop-color:#00CDD7"/>
|
|
||||||
<stop offset="0.9409" style="stop-color:#2086D7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_1_);" points="9.4,63.3 0,7.3 17.5,0.1 28.6,6.7 38.8,1.2 60.1,9.4 48.1,70 "/>
|
|
||||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="30.7199" y1="9.7343" x2="61.365" y2="54.6713">
|
|
||||||
<stop offset="0.1398" style="stop-color:#FFF045"/>
|
|
||||||
<stop offset="0.3656" style="stop-color:#00CDD7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_2_);" points="70,23.7 61,1.4 44.6,0 19.3,24.3 26.1,55.6 38.8,64.6 70,46 62.3,31.7 "/>
|
|
||||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="61.0819" y1="15.2899" x2="65.1065" y2="29.5436">
|
|
||||||
<stop offset="0.2849" style="stop-color:#00CDD7"/>
|
|
||||||
<stop offset="0.9409" style="stop-color:#2086D7"/>
|
|
||||||
</linearGradient>
|
|
||||||
<polygon style="fill:url(#SVGID_3_);" points="56,20.4 62.3,31.7 70,23.7 64.4,9.8 "/>
|
|
||||||
</g>
|
|
||||||
<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"/>
|
|
||||||
<path style="fill:#FFFFFF;" d="M38.7,34.3l2.3-2.8c1.6,1.3,3.3,2.2,5.3,2.2c1.6,0,2.5-0.6,2.5-1.7v-0.1c0-1-0.6-1.5-3.6-2.3
|
|
||||||
c-3.6-0.9-5.8-1.9-5.8-5.5v-0.1c0-3.3,2.6-5.4,6.2-5.4c2.6,0,4.8,0.8,6.6,2.3l-2,3c-1.6-1.1-3.1-1.8-4.6-1.8
|
|
||||||
c-1.5,0-2.3,0.7-2.3,1.6v0.1c0,1.2,0.8,1.6,3.8,2.4c3.6,1,5.6,2.3,5.6,5.4v0.1c0,3.6-2.7,5.6-6.5,5.6
|
|
||||||
C43.5,37.2,40.8,36.2,38.7,34.3"/>
|
|
||||||
</g>
|
|
||||||
<polygon style="fill:#FFFFFF;" points="35.2,19 32.5,29.4 29.5,19 26.5,19 23.4,29.4 20.7,19 16.6,19 21.7,36.9 25,36.9 28,26.5
|
|
||||||
30.9,36.9 34.3,36.9 39.4,19 "/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -9,13 +9,13 @@
|
|||||||
[](#mega-sponsors)
|
[](#mega-sponsors)
|
||||||
|
|
||||||
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
|
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
|
||||||
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
|
Note that only one type of a given movie is supported. If you want both a 4k version and 1080p version of a given movie you will need multiple instances.
|
||||||
|
|
||||||
## Major Features Include
|
## Major Features Include
|
||||||
|
|
||||||
* Adding new movies with lots of information, such as trailers, ratings, etc.
|
* Adding new movies with lots of information, such as trailers, ratings, etc.
|
||||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||||
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
|
* Can watch for better quality of the movies you have and do an automatic upgrade. _eg. from DVD to Blu-Ray_
|
||||||
* Automatic failed download handling will try another release if one fails
|
* Automatic failed download handling will try another release if one fails
|
||||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||||
* Full integration with SABnzbd and NZBGet
|
* Full integration with SABnzbd and NZBGet
|
||||||
@@ -68,12 +68,12 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
|
|||||||
|
|
||||||
## JetBrains
|
## JetBrains
|
||||||
|
|
||||||
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
Thank you to [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jetbrains.png" alt="JetBrains" width="96">](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
|
||||||
|
|
||||||
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/ReSharper_icon.png" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
|
||||||
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/WebStorm_icon.png" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
|
||||||
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/Rider_icon.png" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
|
||||||
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
* [<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/dotTrace_icon.png" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
|
||||||
|
|
||||||
## DigitalOcean
|
## DigitalOcean
|
||||||
|
|
||||||
@@ -87,4 +87,4 @@ This project is also supported by DigitalOcean
|
|||||||
### License
|
### License
|
||||||
|
|
||||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||||
* Copyright 2010-2022
|
* Copyright 2010-2025
|
||||||
|
|||||||
+11
-11
@@ -9,18 +9,18 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.10.3'
|
majorVersion: '5.19.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.424'
|
dotnetVersion: '6.0.427'
|
||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-20.04'
|
||||||
macImage: 'macOS-12'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
branches:
|
branches:
|
||||||
@@ -1116,20 +1116,20 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'radarr'
|
organization: 'radarr'
|
||||||
scannerMode: 'CLI'
|
scannerMode: 'cli'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'Radarr_Radarr.UI'
|
cliProjectKey: 'Radarr_Radarr.UI'
|
||||||
cliProjectName: 'RadarrUI'
|
cliProjectName: 'RadarrUI'
|
||||||
cliProjectVersion: '$(radarrVersion)'
|
cliProjectVersion: '$(radarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
@@ -1205,12 +1205,12 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'radarr'
|
organization: 'radarr'
|
||||||
scannerMode: 'MSBuild'
|
scannerMode: 'dotnet'
|
||||||
projectKey: 'Radarr_Radarr'
|
projectKey: 'Radarr_Radarr'
|
||||||
projectName: 'Radarr'
|
projectName: 'Radarr'
|
||||||
projectVersion: '$(radarrVersion)'
|
projectVersion: '$(radarrVersion)'
|
||||||
@@ -1223,10 +1223,10 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5
|
- task: reportgenerator@5.3.11
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
ARCHITECTURE="${2:-x64}"
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-x64"
|
RUNTIME="win-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-x64"
|
RUNTIME="linux-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-x64"
|
RUNTIME="osx-$ARCHITECTURE"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
|||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
|
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ module.exports = {
|
|||||||
'no-undef-init': 'off',
|
'no-undef-init': 'off',
|
||||||
'no-undefined': 'off',
|
'no-undefined': 'off',
|
||||||
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
||||||
'no-use-before-define': 'error',
|
|
||||||
|
|
||||||
// Node.js and CommonJS
|
// Node.js and CommonJS
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ module.exports = (env) => {
|
|||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
|
target: 'web',
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
@@ -133,6 +134,12 @@ module.exports = (env) => {
|
|||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, 'Content/robots.txt')
|
destination: path.join(distFolder, 'Content/robots.txt')
|
||||||
|
},
|
||||||
|
|
||||||
|
// manifest.json and browserconfig.xml
|
||||||
|
{
|
||||||
|
source: 'frontend/src/Content/*.(json|xml)',
|
||||||
|
destination: path.join(distFolder, 'Content')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -180,7 +187,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.39'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,354 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
|
||||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './HistoryDetails.css';
|
|
||||||
|
|
||||||
function HistoryDetails(props) {
|
|
||||||
const {
|
|
||||||
eventType,
|
|
||||||
sourceTitle,
|
|
||||||
data,
|
|
||||||
downloadId,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (eventType === 'grabbed') {
|
|
||||||
const {
|
|
||||||
indexer,
|
|
||||||
releaseGroup,
|
|
||||||
movieMatchType,
|
|
||||||
customFormatScore,
|
|
||||||
nzbInfoUrl,
|
|
||||||
downloadClient,
|
|
||||||
downloadClientName,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
publishedDate
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
indexer ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
releaseGroup ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ReleaseGroup')}
|
|
||||||
data={releaseGroup}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
movieMatchType ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('MovieMatchType')}
|
|
||||||
data={movieMatchType}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
nzbInfoUrl ?
|
|
||||||
<span>
|
|
||||||
<DescriptionListItemTitle>
|
|
||||||
{translate('InfoUrl')}
|
|
||||||
</DescriptionListItemTitle>
|
|
||||||
|
|
||||||
<DescriptionListItemDescription>
|
|
||||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadClientNameInfo ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DownloadClient')}
|
|
||||||
data={downloadClientNameInfo}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
indexer ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('AgeWhenGrabbed')}
|
|
||||||
data={formatAge(age, ageHours, ageMinutes)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
publishedDate ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('PublishedDate')}
|
|
||||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFolderImported') {
|
|
||||||
const {
|
|
||||||
customFormatScore,
|
|
||||||
droppedPath,
|
|
||||||
importedPath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
droppedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Source')}
|
|
||||||
data={droppedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
importedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ImportedTo')}
|
|
||||||
data={importedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'movieFileDeleted') {
|
|
||||||
const {
|
|
||||||
reason,
|
|
||||||
customFormatScore
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
let reasonMessage = '';
|
|
||||||
|
|
||||||
switch (reason) {
|
|
||||||
case 'Manual':
|
|
||||||
reasonMessage = translate('DeletedReasonManual');
|
|
||||||
break;
|
|
||||||
case 'MissingFromDisk':
|
|
||||||
reasonMessage = translate('DeletedReasonMissingFromDisk');
|
|
||||||
break;
|
|
||||||
case 'Upgrade':
|
|
||||||
reasonMessage = translate('DeletedReasonUpgrade');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reasonMessage = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Reason')}
|
|
||||||
data={reasonMessage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'movieFileRenamed') {
|
|
||||||
const {
|
|
||||||
sourcePath,
|
|
||||||
sourceRelativePath,
|
|
||||||
path,
|
|
||||||
relativePath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourcePath')}
|
|
||||||
data={sourcePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourceRelativePath')}
|
|
||||||
data={sourceRelativePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationPath')}
|
|
||||||
data={path}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationRelativePath')}
|
|
||||||
data={relativePath}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadIgnored') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryDetails.propTypes = {
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryDetails;
|
|
||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||||
|
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import {
|
||||||
|
DownloadFailedHistory,
|
||||||
|
DownloadFolderImportedHistory,
|
||||||
|
DownloadIgnoredHistory,
|
||||||
|
GrabbedHistoryData,
|
||||||
|
HistoryData,
|
||||||
|
HistoryEventType,
|
||||||
|
MovieFileDeletedHistory,
|
||||||
|
MovieFileRenamedHistory,
|
||||||
|
} from 'typings/History';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
|
interface HistoryDetailsProps {
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetails(props: HistoryDetailsProps) {
|
||||||
|
const { eventType, sourceTitle, data, downloadId } = props;
|
||||||
|
|
||||||
|
const { shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventType === 'grabbed') {
|
||||||
|
const {
|
||||||
|
indexer,
|
||||||
|
releaseGroup,
|
||||||
|
movieMatchType,
|
||||||
|
releaseSource,
|
||||||
|
customFormatScore,
|
||||||
|
nzbInfoUrl,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientName,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishedDate,
|
||||||
|
} = data as GrabbedHistoryData;
|
||||||
|
|
||||||
|
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||||
|
|
||||||
|
let releaseSourceMessage = '';
|
||||||
|
|
||||||
|
switch (releaseSource) {
|
||||||
|
case 'Unknown':
|
||||||
|
releaseSourceMessage = translate('Unknown');
|
||||||
|
break;
|
||||||
|
case 'Rss':
|
||||||
|
releaseSourceMessage = translate('Rss');
|
||||||
|
break;
|
||||||
|
case 'Search':
|
||||||
|
releaseSourceMessage = translate('Search');
|
||||||
|
break;
|
||||||
|
case 'UserInvokedSearch':
|
||||||
|
releaseSourceMessage = translate('UserInvokedSearch');
|
||||||
|
break;
|
||||||
|
case 'InteractiveSearch':
|
||||||
|
releaseSourceMessage = translate('InteractiveSearch');
|
||||||
|
break;
|
||||||
|
case 'ReleasePush':
|
||||||
|
releaseSourceMessage = translate('ReleasePush');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
releaseSourceMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{indexer ? (
|
||||||
|
<DescriptionListItem title={translate('Indexer')} data={indexer} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseGroup ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseGroup')}
|
||||||
|
data={releaseGroup}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{movieMatchType ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('MovieMatchType')}
|
||||||
|
data={movieMatchType}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseSource ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseSource')}
|
||||||
|
data={releaseSourceMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{nzbInfoUrl ? (
|
||||||
|
<span>
|
||||||
|
<DescriptionListItemTitle>
|
||||||
|
{translate('InfoUrl')}
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription>
|
||||||
|
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadClientNameInfo ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={downloadClientNameInfo}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{age || ageHours || ageMinutes ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('AgeWhenGrabbed')}
|
||||||
|
data={formatAge(age, ageHours, ageMinutes)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{publishedDate ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('PublishedDate')}
|
||||||
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFailed') {
|
||||||
|
const { message } = data as DownloadFailedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFolderImported') {
|
||||||
|
const { customFormatScore, droppedPath, importedPath } =
|
||||||
|
data as DownloadFolderImportedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{droppedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Source')}
|
||||||
|
data={droppedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{importedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ImportedTo')}
|
||||||
|
data={importedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'movieFileDeleted') {
|
||||||
|
const { reason, customFormatScore } = data as MovieFileDeletedHistory;
|
||||||
|
|
||||||
|
let reasonMessage = '';
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case 'Manual':
|
||||||
|
reasonMessage = translate('DeletedReasonManual');
|
||||||
|
break;
|
||||||
|
case 'MissingFromDisk':
|
||||||
|
reasonMessage = translate('DeletedReasonMovieMissingFromDisk');
|
||||||
|
break;
|
||||||
|
case 'Upgrade':
|
||||||
|
reasonMessage = translate('DeletedReasonUpgrade');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reasonMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('Reason')} data={reasonMessage} />
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'movieFileRenamed') {
|
||||||
|
const { sourcePath, sourceRelativePath, path, relativePath } =
|
||||||
|
data as MovieFileRenamedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourcePath')}
|
||||||
|
data={sourcePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourceRelativePath')}
|
||||||
|
data={sourceRelativePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('DestinationPath')} data={path} />
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DestinationRelativePath')}
|
||||||
|
data={relativePath}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadIgnored') {
|
||||||
|
const { message } = data as DownloadIgnoredHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryDetails;
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import HistoryDetails from './HistoryDetails';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(uiSettings) => {
|
|
||||||
return {
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(HistoryDetails);
|
|
||||||
+29
-49
@@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
@@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import HistoryDetails from './HistoryDetails';
|
import HistoryDetails from './HistoryDetails';
|
||||||
import styles from './HistoryDetailsModal.css';
|
import styles from './HistoryDetailsModal.css';
|
||||||
|
|
||||||
function getHeaderTitle(eventType) {
|
function getHeaderTitle(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('Grabbed');
|
return translate('Grabbed');
|
||||||
@@ -31,29 +31,33 @@ function getHeaderTitle(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryDetailsModal(props) {
|
interface HistoryDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
isMarkingAsFailed: boolean;
|
||||||
|
onMarkAsFailedPress: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
eventType,
|
eventType,
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
data,
|
data,
|
||||||
downloadId,
|
downloadId,
|
||||||
isMarkingAsFailed,
|
isMarkingAsFailed = false,
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onMarkAsFailedPress,
|
onMarkAsFailedPress,
|
||||||
onModalClose
|
onModalClose,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>{getHeaderTitle(eventType)}</ModalHeader>
|
||||||
{getHeaderTitle(eventType)}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<HistoryDetails
|
<HistoryDetails
|
||||||
@@ -61,50 +65,26 @@ function HistoryDetailsModal(props) {
|
|||||||
sourceTitle={sourceTitle}
|
sourceTitle={sourceTitle}
|
||||||
data={data}
|
data={data}
|
||||||
downloadId={downloadId}
|
downloadId={downloadId}
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{
|
{eventType === 'grabbed' && (
|
||||||
eventType === 'grabbed' &&
|
<SpinnerButton
|
||||||
<SpinnerButton
|
className={styles.markAsFailedButton}
|
||||||
className={styles.markAsFailedButton}
|
kind={kinds.DANGER}
|
||||||
kind={kinds.DANGER}
|
isSpinning={isMarkingAsFailed}
|
||||||
isSpinning={isMarkingAsFailed}
|
onPress={onMarkAsFailedPress}
|
||||||
onPress={onMarkAsFailedPress}
|
>
|
||||||
>
|
{translate('MarkAsFailed')}
|
||||||
{translate('MarkAsFailed')}
|
</SpinnerButton>
|
||||||
</SpinnerButton>
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryDetailsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryDetailsModal.defaultProps = {
|
|
||||||
isMarkingAsFailed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryDetailsModal;
|
export default HistoryDetailsModal;
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import TablePager from 'Components/Table/TablePager';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import HistoryFilterModal from './HistoryFilterModal';
|
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
|
||||||
|
|
||||||
class History extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isMoviesFetching,
|
|
||||||
isMoviesPopulated,
|
|
||||||
moviesError,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
totalRecords,
|
|
||||||
onFilterSelect,
|
|
||||||
onFirstPagePress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isFetchingAny = isFetching || isMoviesFetching;
|
|
||||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
|
|
||||||
const hasError = error || moviesError;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('History')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Refresh')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isFetching}
|
|
||||||
onPress={onFirstPagePress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
{...otherProps}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={HistoryFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isFetchingAny && !isAllPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetchingAny && hasError &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('HistoryLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// If history isPopulated and it's empty show no history found and don't
|
|
||||||
// wait for the episodes to populate because they are never coming.
|
|
||||||
|
|
||||||
isPopulated && !hasError && !items.length &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('NoHistoryFound')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !!items.length &&
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<HistoryRowConnector
|
|
||||||
key={item.id}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetching}
|
|
||||||
onFirstPagePress={onFirstPagePress}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
History.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
isMoviesFetching: PropTypes.bool.isRequired,
|
|
||||||
isMoviesPopulated: PropTypes.bool.isRequired,
|
|
||||||
moviesError: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
|
||||||
onFirstPagePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default History;
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import usePaging from 'Components/Table/usePaging';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
|
||||||
|
import {
|
||||||
|
clearHistory,
|
||||||
|
fetchHistory,
|
||||||
|
gotoHistoryPage,
|
||||||
|
setHistoryFilter,
|
||||||
|
setHistorySort,
|
||||||
|
setHistoryTableOption,
|
||||||
|
} from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
|
import HistoryRow from './HistoryRow';
|
||||||
|
|
||||||
|
function History() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
} = useSelector((state: AppState) => state.history);
|
||||||
|
|
||||||
|
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
|
||||||
|
createMoviesFetchingSelector()
|
||||||
|
);
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('history'));
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const isFetchingAny = isFetching || isMoviesFetching;
|
||||||
|
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
|
||||||
|
const hasError = error || moviesError;
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoHistoryPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string) => {
|
||||||
|
dispatch(setHistoryFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setHistorySort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setHistoryTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoHistoryPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoHistoryPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearHistory());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('History')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Refresh')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
onPress={handleFirstPagePress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{isFetchingAny && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetchingAny && hasError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{
|
||||||
|
// If history isPopulated and it's empty show no history found and don't
|
||||||
|
// wait for the movies to populate because they are never coming.
|
||||||
|
|
||||||
|
isPopulated && !hasError && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<HistoryRow key={item.id} columns={columns} {...item} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import History from './History';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.history,
|
|
||||||
(state) => state.movies,
|
|
||||||
createCustomFiltersSelector('history'),
|
|
||||||
(history, movies, customFilters) => {
|
|
||||||
return {
|
|
||||||
isMoviesFetching: movies.isFetching,
|
|
||||||
isMoviesPopulated: movies.isPopulated,
|
|
||||||
moviesError: movies.error,
|
|
||||||
customFilters,
|
|
||||||
...history
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...historyActions
|
|
||||||
};
|
|
||||||
|
|
||||||
class HistoryConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchHistory,
|
|
||||||
gotoHistoryFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchHistory();
|
|
||||||
} else {
|
|
||||||
gotoHistoryFirstPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchHistory();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoHistoryFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoHistoryPreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoHistoryNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoHistoryLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoHistoryPage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setHistorySort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setHistoryFilter({ selectedFilterKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setHistoryTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoHistoryFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<History
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryConnector.propTypes = {
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchHistory: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryPreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryPage: PropTypes.func.isRequired,
|
|
||||||
setHistorySort: PropTypes.func.isRequired,
|
|
||||||
setHistoryFilter: PropTypes.func.isRequired,
|
|
||||||
setHistoryTableOption: PropTypes.func.isRequired,
|
|
||||||
clearHistory: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
|
|
||||||
);
|
|
||||||
+29
-27
@@ -1,12 +1,17 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
GrabbedHistoryData,
|
||||||
|
HistoryData,
|
||||||
|
HistoryEventType,
|
||||||
|
MovieFileDeletedHistory,
|
||||||
|
} from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType, data) {
|
function getIconName(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@@ -17,7 +22,9 @@ function getIconName(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? icons.FILE_MISSING
|
||||||
|
: icons.DELETE;
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -27,7 +34,7 @@ function getIconName(eventType, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconKind(eventType) {
|
function getIconKind(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return kinds.DANGER;
|
return kinds.DANGER;
|
||||||
@@ -36,52 +43,47 @@ function getIconKind(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTooltip(eventType, data) {
|
function getTooltip(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
|
return translate('MovieGrabbedTooltip', {
|
||||||
|
indexer: (data as GrabbedHistoryData).indexer,
|
||||||
|
downloadClient: (data as GrabbedHistoryData).downloadClient,
|
||||||
|
});
|
||||||
case 'movieFolderImported':
|
case 'movieFolderImported':
|
||||||
return translate('MovieFolderImportedTooltip');
|
return translate('MovieFolderImportedTooltip');
|
||||||
case 'downloadFolderImported':
|
case 'downloadFolderImported':
|
||||||
return translate('MovieImportedTooltip');
|
return translate('MovieImportedTooltip');
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('MovieDownloadFailedTooltip');
|
return translate('DownloadFailedMovieTooltip');
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
|
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? translate('MovieFileMissingTooltip')
|
||||||
|
: translate('MovieFileDeletedTooltip');
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return translate('MovieFileRenamedTooltip');
|
return translate('MovieFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
return translate('MovieDownloadIgnoredTooltip');
|
return translate('DownloadIgnoredMovieTooltip');
|
||||||
default:
|
default:
|
||||||
return translate('UnknownEventTooltip');
|
return translate('UnknownEventTooltip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
interface HistoryEventTypeCellProps {
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
data: HistoryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
|
||||||
const iconName = getIconName(eventType, data);
|
const iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.cell} title={tooltip}>
|
||||||
className={styles.cell}
|
<Icon name={iconName} kind={iconKind} />
|
||||||
title={tooltip}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryEventTypeCell.propTypes = {
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryEventTypeCell.defaultProps = {
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryEventTypeCell;
|
export default HistoryEventTypeCell;
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
|
||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
|
||||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
|
||||||
import styles from './HistoryRow.css';
|
|
||||||
|
|
||||||
class HistoryRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
prevProps.isMarkingAsFailed &&
|
|
||||||
!this.props.isMarkingAsFailed &&
|
|
||||||
!this.props.markAsFailedError
|
|
||||||
) {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onDetailsPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
movie,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
customFormatScore,
|
|
||||||
languages,
|
|
||||||
qualityCutoffNotMet,
|
|
||||||
eventType,
|
|
||||||
sourceTitle,
|
|
||||||
date,
|
|
||||||
data,
|
|
||||||
downloadId,
|
|
||||||
isMarkingAsFailed,
|
|
||||||
columns,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onMarkAsFailedPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'eventType') {
|
|
||||||
return (
|
|
||||||
<HistoryEventTypeCell
|
|
||||||
key={name}
|
|
||||||
eventType={eventType}
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'movieMetadata.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieTitleLink
|
|
||||||
titleSlug={movie.titleSlug}
|
|
||||||
title={movie.title}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieLanguages
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieQuality
|
|
||||||
quality={quality}
|
|
||||||
isCutoffMet={qualityCutoffNotMet}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'date') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCell
|
|
||||||
key={name}
|
|
||||||
date={date}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.downloadClient}
|
|
||||||
>
|
|
||||||
{data.downloadClient}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexer}
|
|
||||||
>
|
|
||||||
{data.indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormatScore') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.customFormatScore}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<MovieFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'releaseGroup') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.releaseGroup}
|
|
||||||
>
|
|
||||||
{data.releaseGroup}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sourceTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
>
|
|
||||||
{sourceTitle}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'details') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.details}
|
|
||||||
>
|
|
||||||
<div className={styles.actionContents}>
|
|
||||||
<IconButton
|
|
||||||
name={icons.INFO}
|
|
||||||
onPress={this.onDetailsPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<HistoryDetailsModal
|
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
|
||||||
eventType={eventType}
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
data={data}
|
|
||||||
downloadId={downloadId}
|
|
||||||
isMarkingAsFailed={isMarkingAsFailed}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryRow.propTypes = {
|
|
||||||
movieId: PropTypes.number,
|
|
||||||
movie: PropTypes.object.isRequired,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
isMarkingAsFailed: PropTypes.bool,
|
|
||||||
markAsFailedError: PropTypes.object,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryRow.defaultProps = {
|
|
||||||
customFormats: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryRow;
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import useMovie from 'Movie/useMovie';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
|
import styles from './HistoryRow.css';
|
||||||
|
|
||||||
|
interface HistoryRowProps {
|
||||||
|
id: number;
|
||||||
|
movieId: number;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats?: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
qualityCutoffNotMet: boolean;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
date: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
isMarkingAsFailed?: boolean;
|
||||||
|
markAsFailedError?: object;
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryRow(props: HistoryRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieId,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats = [],
|
||||||
|
customFormatScore,
|
||||||
|
qualityCutoffNotMet,
|
||||||
|
eventType,
|
||||||
|
sourceTitle,
|
||||||
|
date,
|
||||||
|
data,
|
||||||
|
downloadId,
|
||||||
|
isMarkingAsFailed = false,
|
||||||
|
markAsFailedError,
|
||||||
|
columns,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const movie = useMovie(movieId);
|
||||||
|
|
||||||
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDetailsPress = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(true);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleDetailsModalClose = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleMarkAsFailedPress = useCallback(() => {
|
||||||
|
dispatch(markAsFailed({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
wasMarkingAsFailed,
|
||||||
|
isMarkingAsFailed,
|
||||||
|
markAsFailedError,
|
||||||
|
setIsDetailsModalOpen,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'eventType') {
|
||||||
|
return (
|
||||||
|
<HistoryEventTypeCell
|
||||||
|
key={name}
|
||||||
|
eventType={eventType}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieQuality
|
||||||
|
quality={quality}
|
||||||
|
isCutoffNotMet={qualityCutoffNotMet}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
return <RelativeDateCell key={name} date={date} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
const downloadClientName =
|
||||||
|
'downloadClientName' in data ? data.downloadClientName : null;
|
||||||
|
const downloadClient =
|
||||||
|
'downloadClient' in data ? data.downloadClient : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.downloadClient}>
|
||||||
|
{downloadClientName ?? downloadClient ?? ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.indexer}>
|
||||||
|
{'indexer' in data ? data.indexer : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'releaseGroup') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.releaseGroup}>
|
||||||
|
{'releaseGroup' in data ? data.releaseGroup : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'details') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.details}>
|
||||||
|
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<HistoryDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
eventType={eventType}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
data={data}
|
||||||
|
downloadId={downloadId}
|
||||||
|
isMarkingAsFailed={isMarkingAsFailed}
|
||||||
|
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryRow;
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import HistoryRow from './HistoryRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(movie, uiSettings) => {
|
|
||||||
return {
|
|
||||||
movie,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchHistory,
|
|
||||||
markAsFailed
|
|
||||||
};
|
|
||||||
|
|
||||||
class HistoryRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
prevProps.isMarkingAsFailed &&
|
|
||||||
!this.props.isMarkingAsFailed &&
|
|
||||||
!this.props.markAsFailedError
|
|
||||||
) {
|
|
||||||
this.props.fetchHistory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMarkAsFailedPress = () => {
|
|
||||||
this.props.markAsFailed({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<HistoryRow
|
|
||||||
{...this.props}
|
|
||||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryRowConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
isMarkingAsFailed: PropTypes.bool,
|
|
||||||
markAsFailedError: PropTypes.object,
|
|
||||||
fetchHistory: PropTypes.func.isRequired,
|
|
||||||
markAsFailed: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);
|
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import Icon, { IconProps } from 'Components/Icon';
|
import Icon, { IconProps } from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
|
||||||
import {
|
import {
|
||||||
QueueTrackedDownloadState,
|
QueueTrackedDownloadState,
|
||||||
QueueTrackedDownloadStatus,
|
QueueTrackedDownloadStatus,
|
||||||
|
|||||||
@@ -131,7 +131,9 @@ class AddNewMovie extends Component {
|
|||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
|
|
||||||
|
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
||||||
{translate('WhySearchesCouldBeFailing')}
|
{translate('WhySearchesCouldBeFailing')}
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import Form from 'Components/Form/Form';
|
import Form from 'Components/Form/Form';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './AddNewMovieModalContent.css';
|
import styles from './AddNewMovieModalContent.css';
|
||||||
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
|
<FormLabel>
|
||||||
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.labelIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.AVAILABILITY_SELECT}
|
type={inputTypes.AVAILABILITY_SELECT}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...minimumAvailability}
|
{...minimumAvailability}
|
||||||
|
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,10 @@
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.genres {
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
|||||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||||
import MovieStatusLabel from 'Movie/Details/MovieStatusLabel';
|
import MovieStatusLabel from 'Movie/Details/MovieStatusLabel';
|
||||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||||
|
import MovieGenres from 'Movie/MovieGenres';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import formatRuntime from 'Utilities/Date/formatRuntime';
|
import formatRuntime from 'Utilities/Date/formatRuntime';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
@@ -249,9 +250,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
name={icons.GENRE}
|
name={icons.GENRE}
|
||||||
size={13}
|
size={13}
|
||||||
/>
|
/>
|
||||||
<span className={styles.genres}>
|
<MovieGenres className={styles.genres} genres={genres} />
|
||||||
{genres.slice(0, 3).join(', ')}
|
|
||||||
</span>
|
|
||||||
</Label> :
|
</Label> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -280,7 +279,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
}
|
}
|
||||||
canFlip={true}
|
canFlip={true}
|
||||||
kind={kinds.INVERSE}
|
kind={kinds.INVERSE}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.TOP}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ImportMovieHeader.css';
|
import styles from './ImportMovieHeader.css';
|
||||||
|
|
||||||
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
|
|||||||
className={styles.minimumAvailability}
|
className={styles.minimumAvailability}
|
||||||
name="minimumAvailability"
|
name="minimumAvailability"
|
||||||
>
|
>
|
||||||
{translate('MinAvailability')}
|
{translate('MinimumAvailability')}
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
className={styles.detailsIcon}
|
||||||
|
name={icons.INFO}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={translate('MinimumAvailability')}
|
||||||
|
body={<MovieMinimumAvailabilityPopoverContent />}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
|
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
|
|||||||
@@ -81,7 +81,6 @@ ImportMovieRow.propTypes = {
|
|||||||
selectedMovie: PropTypes.object,
|
selectedMovie: PropTypes.object,
|
||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
queued: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ImportMovieSelectMovie extends Component {
|
|||||||
id={this._buttonId}
|
id={this._buttonId}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
component="div"
|
component="div"
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
@@ -255,7 +255,7 @@ class ImportMovieSelectMovie extends Component {
|
|||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<ImportMovieSearchResultConnector
|
<ImportMovieSearchResultConnector
|
||||||
key={item.tvdbId}
|
key={item.tmdbId}
|
||||||
tmdbId={item.tmdbId}
|
tmdbId={item.tmdbId}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
year={item.year}
|
year={item.year}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
|
function MovieMinimumAvailabilityPopoverContent() {
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Announced')}
|
||||||
|
data={translate('AnnouncedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('InCinemas')}
|
||||||
|
data={translate('InCinemasMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Released')}
|
||||||
|
data={translate('ReleasedMovieAvailabilityDescription')}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieMinimumAvailabilityPopoverContent;
|
||||||
@@ -1,20 +1,25 @@
|
|||||||
import { ConnectedRouter } from 'connected-react-router';
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DocumentTitle from 'react-document-title';
|
import DocumentTitle from 'react-document-title';
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from 'react-redux';
|
||||||
|
import { Store } from 'redux';
|
||||||
import PageConnector from 'Components/Page/PageConnector';
|
import PageConnector from 'Components/Page/PageConnector';
|
||||||
import ApplyTheme from './ApplyTheme';
|
import ApplyTheme from './ApplyTheme';
|
||||||
import AppRoutes from './AppRoutes';
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
function App({ store, history }) {
|
interface AppProps {
|
||||||
|
store: Store;
|
||||||
|
history: ConnectedRouterProps['history'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function App({ store, history }: AppProps) {
|
||||||
return (
|
return (
|
||||||
<DocumentTitle title={window.Radarr.instanceName}>
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<ConnectedRouter history={history}>
|
<ConnectedRouter history={history}>
|
||||||
<ApplyTheme />
|
<ApplyTheme />
|
||||||
<PageConnector>
|
<PageConnector>
|
||||||
<AppRoutes app={App} />
|
<AppRoutes />
|
||||||
</PageConnector>
|
</PageConnector>
|
||||||
</ConnectedRouter>
|
</ConnectedRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
@@ -22,9 +27,4 @@ function App({ store, history }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
App.propTypes = {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
|
||||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
|
||||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
|
||||||
import Queue from 'Activity/Queue/Queue';
|
|
||||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
|
||||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
|
||||||
import CollectionConnector from 'Collection/CollectionConnector';
|
|
||||||
import NotFound from 'Components/NotFound';
|
|
||||||
import Switch from 'Components/Router/Switch';
|
|
||||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
|
||||||
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
|
|
||||||
import MovieIndex from 'Movie/Index/MovieIndex';
|
|
||||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
|
||||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
|
||||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
|
||||||
import Profiles from 'Settings/Profiles/Profiles';
|
|
||||||
import QualityConnector from 'Settings/Quality/QualityConnector';
|
|
||||||
import Settings from 'Settings/Settings';
|
|
||||||
import TagSettings from 'Settings/Tags/TagSettings';
|
|
||||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
|
||||||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
|
||||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|
||||||
import Logs from 'System/Logs/Logs';
|
|
||||||
import Status from 'System/Status/Status';
|
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
|
||||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
|
||||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
|
||||||
|
|
||||||
function AppRoutes(props) {
|
|
||||||
const {
|
|
||||||
app
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{/*
|
|
||||||
Movies
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
component={MovieIndex}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
window.Radarr.urlBase &&
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
addUrlBase={false}
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={getPathWithUrlBase('/')}
|
|
||||||
component={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/new"
|
|
||||||
component={AddNewMovieConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/collections"
|
|
||||||
component={CollectionConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/import"
|
|
||||||
component={ImportMovies}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/discover"
|
|
||||||
component={DiscoverMovieConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/movie/:titleSlug"
|
|
||||||
component={MovieDetailsPageConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Calendar
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/calendar"
|
|
||||||
component={CalendarPageConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Activity
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/history"
|
|
||||||
component={HistoryConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/queue"
|
|
||||||
component={Queue}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/blocklist"
|
|
||||||
component={Blocklist}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Wanted
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/missing"
|
|
||||||
component={MissingConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/cutoffunmet"
|
|
||||||
component={CutoffUnmetConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Settings
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/settings"
|
|
||||||
component={Settings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/mediamanagement"
|
|
||||||
component={MediaManagementConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/profiles"
|
|
||||||
component={Profiles}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/quality"
|
|
||||||
component={QualityConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/customformats"
|
|
||||||
component={CustomFormatSettingsPage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/indexers"
|
|
||||||
component={IndexerSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/downloadclients"
|
|
||||||
component={DownloadClientSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/importlists"
|
|
||||||
component={ImportListSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/connect"
|
|
||||||
component={NotificationSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/metadata"
|
|
||||||
component={MetadataSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/tags"
|
|
||||||
component={TagSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/general"
|
|
||||||
component={GeneralSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/ui"
|
|
||||||
component={UISettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
System
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/status"
|
|
||||||
component={Status}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/tasks"
|
|
||||||
component={Tasks}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/backup"
|
|
||||||
component={BackupsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/updates"
|
|
||||||
component={UpdatesConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/events"
|
|
||||||
component={LogsTableConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/logs/files"
|
|
||||||
component={Logs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Not Found
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="*"
|
|
||||||
component={NotFound}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppRoutes.propTypes = {
|
|
||||||
app: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppRoutes;
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
|
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||||
|
import History from 'Activity/History/History';
|
||||||
|
import Queue from 'Activity/Queue/Queue';
|
||||||
|
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||||
|
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||||
|
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||||
|
import CollectionConnector from 'Collection/CollectionConnector';
|
||||||
|
import NotFound from 'Components/NotFound';
|
||||||
|
import Switch from 'Components/Router/Switch';
|
||||||
|
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||||
|
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
|
||||||
|
import MovieIndex from 'Movie/Index/MovieIndex';
|
||||||
|
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||||
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
|
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||||
|
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||||
|
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||||
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
|
import Profiles from 'Settings/Profiles/Profiles';
|
||||||
|
import QualityConnector from 'Settings/Quality/QualityConnector';
|
||||||
|
import Settings from 'Settings/Settings';
|
||||||
|
import TagSettings from 'Settings/Tags/TagSettings';
|
||||||
|
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||||
|
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||||
|
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
|
import Logs from 'System/Logs/Logs';
|
||||||
|
import Status from 'System/Status/Status';
|
||||||
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
|
import Updates from 'System/Updates/Updates';
|
||||||
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
||||||
|
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
||||||
|
|
||||||
|
function RedirectWithUrlBase() {
|
||||||
|
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{/*
|
||||||
|
Movies
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/" component={MovieIndex} />
|
||||||
|
|
||||||
|
{window.Radarr.urlBase && (
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
addUrlBase={false}
|
||||||
|
render={RedirectWithUrlBase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Route path="/add/new" component={AddNewMovieConnector} />
|
||||||
|
|
||||||
|
<Route path="/collections" component={CollectionConnector} />
|
||||||
|
|
||||||
|
<Route path="/add/import" component={ImportMovies} />
|
||||||
|
|
||||||
|
<Route path="/add/discover" component={DiscoverMovieConnector} />
|
||||||
|
|
||||||
|
<Route path="/movie/:titleSlug" component={MovieDetailsPageConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Calendar
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/calendar" component={CalendarPageConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Activity
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/activity/history" component={History} />
|
||||||
|
|
||||||
|
<Route path="/activity/queue" component={Queue} />
|
||||||
|
|
||||||
|
<Route path="/activity/blocklist" component={Blocklist} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Wanted
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/wanted/missing" component={MissingConnector} />
|
||||||
|
|
||||||
|
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Settings
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/settings" component={Settings} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/mediamanagement"
|
||||||
|
component={MediaManagementConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/profiles" component={Profiles} />
|
||||||
|
|
||||||
|
<Route path="/settings/quality" component={QualityConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/customformats"
|
||||||
|
component={CustomFormatSettingsPage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/downloadclients"
|
||||||
|
component={DownloadClientSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/importlists"
|
||||||
|
component={ImportListSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/connect" component={NotificationSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/metadata" component={MetadataSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/tags" component={TagSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/general" component={GeneralSettingsConnector} />
|
||||||
|
|
||||||
|
<Route path="/settings/ui" component={UISettingsConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
System
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/system/status" component={Status} />
|
||||||
|
|
||||||
|
<Route path="/system/tasks" component={Tasks} />
|
||||||
|
|
||||||
|
<Route path="/system/backup" component={BackupsConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/updates" component={Updates} />
|
||||||
|
|
||||||
|
<Route path="/system/events" component={LogsTableConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/logs/files" component={Logs} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Not Found
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="*" component={NotFound} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector';
|
|
||||||
|
|
||||||
function AppUpdatedModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnBackgroundClick={false}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AppUpdatedModalContentConnector
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModal;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
||||||
|
|
||||||
|
interface AppUpdatedModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModal(props: AppUpdatedModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AppUpdatedModalContent onModalClose={handleModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import AppUpdatedModal from './AppUpdatedModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(AppUpdatedModal);
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
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) {
|
|
||||||
const {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
onSeeChangesPress,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const update = mergeUpdates(items, version, prevVersion);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AppUpdated')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !!update &&
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
!update.changes &&
|
|
||||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!update.changes &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.changes}>
|
|
||||||
{translate('WhatsNew')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('New')}
|
|
||||||
changes={update.changes.new}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('Fixed')}
|
|
||||||
changes={update.changes.fixed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isPopulated && !error &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onSeeChangesPress}
|
|
||||||
>
|
|
||||||
{translate('RecentChanges')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContent.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
prevVersion: PropTypes.string,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSeeChangesPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModalContent;
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||||
|
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
import styles from './AppUpdatedModalContent.css';
|
||||||
|
|
||||||
|
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
|
||||||
|
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: Update['changes'] = { new: [], fixed: [] };
|
||||||
|
|
||||||
|
appliedUpdates.forEach((u: Update) => {
|
||||||
|
if (u.changes) {
|
||||||
|
appliedChanges.new.push(...u.changes.new);
|
||||||
|
appliedChanges.fixed.push(...u.changes.fixed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
|
||||||
|
changes: appliedChanges,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||||
|
mergedUpdate.changes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppUpdatedModalContentProps {
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { version, prevVersion } = useSelector((state: AppState) => state.app);
|
||||||
|
const { isPopulated, error, items } = useSelector(
|
||||||
|
(state: AppState) => state.system.updates
|
||||||
|
);
|
||||||
|
const previousVersion = usePrevious(version);
|
||||||
|
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const update = mergeUpdates(items, version, prevVersion);
|
||||||
|
|
||||||
|
const handleSeeChangesPress = useCallback(() => {
|
||||||
|
window.location.href = `${window.Radarr.urlBase}/system/updates`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (version !== previousVersion) {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}
|
||||||
|
}, [version, previousVersion, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('AppUpdated')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('AppUpdatedVersion', { version })}
|
||||||
|
blockClassName={styles.version}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPopulated && !error && !!update ? (
|
||||||
|
<div>
|
||||||
|
{update.changes ? (
|
||||||
|
<div className={styles.maintenance}>
|
||||||
|
{translate('MaintenanceRelease')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{update.changes ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.changes}>{translate('WhatsNew')}</div>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('New')}
|
||||||
|
changes={update.changes.new}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('Fixed')}
|
||||||
|
changes={update.changes.fixed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleSeeChangesPress}>
|
||||||
|
{translate('RecentChanges')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onModalClose}>
|
||||||
|
{translate('Reload')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModalContent;
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
|
||||||
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app.version,
|
|
||||||
(state) => state.app.prevVersion,
|
|
||||||
(state) => state.system.updates,
|
|
||||||
(version, prevVersion, updates) => {
|
|
||||||
const {
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = updates;
|
|
||||||
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
dispatchFetchUpdates() {
|
|
||||||
dispatch(fetchUpdates());
|
|
||||||
},
|
|
||||||
|
|
||||||
onSeeChangesPress() {
|
|
||||||
window.location = `${window.Radarr.urlBase}/system/updates`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppUpdatedModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.version !== this.props.version) {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatchFetchUpdates,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppUpdatedModalContent {...otherProps} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContentConnector.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
dispatchFetchUpdates: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);
|
|
||||||
+16
-27
@@ -1,5 +1,4 @@
|
|||||||
import PropTypes from 'prop-types';
|
import React, { useCallback } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
@@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ConnectionLostModal.css';
|
import styles from './ConnectionLostModal.css';
|
||||||
|
|
||||||
function ConnectionLostModal(props) {
|
interface ConnectionLostModalProps {
|
||||||
const {
|
isOpen: boolean;
|
||||||
isOpen,
|
}
|
||||||
onModalClose
|
|
||||||
} = props;
|
function ConnectionLostModal(props: ConnectionLostModalProps) {
|
||||||
|
const { isOpen } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
isOpen={isOpen}
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
onModalClose={onModalClose}
|
<ModalHeader>{translate('ConnectionLost')}</ModalHeader>
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('ConnectionLost')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>{translate('ConnectionLostToBackend')}</div>
|
||||||
{translate('ConnectionLostToBackend')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostReconnect')}
|
{translate('ConnectionLostReconnect')}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button kind={kinds.PRIMARY} onPress={handleModalClose}>
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
{translate('Reload')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -48,9 +42,4 @@ function ConnectionLostModal(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionLostModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectionLostModal;
|
export default ConnectionLostModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ConnectionLostModal from './ConnectionLostModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);
|
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
|
import { ValidationFailure } from 'typings/pending';
|
||||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
status?: number;
|
||||||
message: string;
|
responseJSON:
|
||||||
};
|
| {
|
||||||
|
message: string | undefined;
|
||||||
|
}
|
||||||
|
| ValidationFailure[]
|
||||||
|
| undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionDeleteState {
|
export interface AppSectionDeleteState {
|
||||||
@@ -51,6 +56,16 @@ export interface AppSectionItemState<T> {
|
|||||||
item: T;
|
item: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionProviderState<T>
|
||||||
|
extends AppSectionDeleteState,
|
||||||
|
AppSectionSaveState {
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
items: T[];
|
||||||
|
pendingChanges: Partial<T>;
|
||||||
|
}
|
||||||
|
|
||||||
interface AppSectionState<T> {
|
interface AppSectionState<T> {
|
||||||
isFetching: boolean;
|
isFetching: boolean;
|
||||||
isPopulated: boolean;
|
isPopulated: boolean;
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import HistoryAppState from './HistoryAppState';
|
||||||
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
|
import PathsAppState from './PathsAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
@@ -49,6 +51,7 @@ export interface AppSectionState {
|
|||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
isReconnecting: boolean;
|
isReconnecting: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
@@ -64,10 +67,12 @@ interface AppState {
|
|||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
|
movieCredits: MovieCreditAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
movies: MoviesAppState;
|
||||||
parse: ParseAppState;
|
parse: ParseAppState;
|
||||||
|
paths: PathsAppState;
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import History from 'typings/History';
|
import History from 'typings/History';
|
||||||
|
|
||||||
interface HistoryAppState
|
interface HistoryAppState
|
||||||
extends AppSectionState<History>,
|
extends AppSectionState<History>,
|
||||||
AppSectionFilterState<History> {}
|
AppSectionFilterState<History>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
export default HistoryAppState;
|
export default HistoryAppState;
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
|
|
||||||
import ImportMode from 'InteractiveImport/ImportMode';
|
import ImportMode from 'InteractiveImport/ImportMode';
|
||||||
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
||||||
|
|
||||||
|
interface FavoriteFolder {
|
||||||
|
folder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecentFolder {
|
||||||
|
folder: string;
|
||||||
|
lastUsed: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
||||||
originalItems: InteractiveImport[];
|
originalItems: InteractiveImport[];
|
||||||
importMode: ImportMode;
|
importMode: ImportMode;
|
||||||
|
favoriteFolders: FavoriteFolder[];
|
||||||
recentFolders: RecentFolder[];
|
recentFolders: RecentFolder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
||||||
|
import Metadata from 'typings/Metadata';
|
||||||
|
|
||||||
|
type MetadataAppState = AppSectionProviderState<Metadata>;
|
||||||
|
|
||||||
|
export default MetadataAppState;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
|
||||||
|
type MovieCreditAppState = AppSectionState<MovieCredit>;
|
||||||
|
|
||||||
|
export default MovieCreditAppState;
|
||||||
@@ -3,7 +3,7 @@ import AppSectionState, {
|
|||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import { Filter, FilterBuilderProp } from './AppState';
|
import { Filter, FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ export interface MovieIndexAppState {
|
|||||||
showTmdbRating: boolean;
|
showTmdbRating: boolean;
|
||||||
showImdbRating: boolean;
|
showImdbRating: boolean;
|
||||||
showRottenTomatoesRating: boolean;
|
showRottenTomatoesRating: boolean;
|
||||||
|
showTraktRating: boolean;
|
||||||
showTags: boolean;
|
showTags: boolean;
|
||||||
showSearchAction: boolean;
|
showSearchAction: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
interface BasePath {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface File extends BasePath {
|
||||||
|
type: 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Folder extends BasePath {
|
||||||
|
type: 'folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent';
|
||||||
|
export type Path = File | Folder;
|
||||||
|
|
||||||
|
interface PathsAppState {
|
||||||
|
currentPath: string;
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
directories: Folder[];
|
||||||
|
files: File[];
|
||||||
|
parent: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PathsAppState;
|
||||||
@@ -16,7 +16,11 @@ import IndexerFlag from 'typings/IndexerFlag';
|
|||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import General from 'typings/Settings/General';
|
import General from 'typings/Settings/General';
|
||||||
|
import NamingConfig from 'typings/Settings/NamingConfig';
|
||||||
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import UiSettings from 'typings/Settings/UiSettings';
|
import UiSettings from 'typings/Settings/UiSettings';
|
||||||
|
import MetadataAppState from './MetadataAppState';
|
||||||
|
|
||||||
export interface DownloadClientAppState
|
export interface DownloadClientAppState
|
||||||
extends AppSectionState<DownloadClient>,
|
extends AppSectionState<DownloadClient>,
|
||||||
@@ -29,6 +33,12 @@ export interface GeneralAppState
|
|||||||
extends AppSectionItemState<General>,
|
extends AppSectionItemState<General>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export interface NamingAppState
|
||||||
|
extends AppSectionItemState<NamingConfig>,
|
||||||
|
AppSectionSaveState {}
|
||||||
|
|
||||||
|
export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -49,6 +59,12 @@ export interface QualityProfilesAppState
|
|||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export interface ReleaseProfilesAppState
|
||||||
|
extends AppSectionState<ReleaseProfile>,
|
||||||
|
AppSectionSaveState {
|
||||||
|
pendingChanges: Partial<ReleaseProfile>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomFormatAppState
|
export interface CustomFormatAppState
|
||||||
extends AppSectionState<CustomFormat>,
|
extends AppSectionState<CustomFormat>,
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
@@ -81,8 +97,12 @@ interface SettingsAppState {
|
|||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
metadata: MetadataAppState;
|
||||||
|
naming: NamingAppState;
|
||||||
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
qualityProfiles: QualityProfilesAppState;
|
qualityProfiles: QualityProfilesAppState;
|
||||||
|
releaseProfiles: ReleaseProfilesAppState;
|
||||||
ui: UiSettingsAppState;
|
ui: UiSettingsAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,18 +2,21 @@ import DiskSpace from 'typings/DiskSpace';
|
|||||||
import Health from 'typings/Health';
|
import Health from 'typings/Health';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import Task from 'typings/Task';
|
import Task from 'typings/Task';
|
||||||
|
import Update from 'typings/Update';
|
||||||
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
import AppSectionState, { AppSectionItemState } from './AppSectionState';
|
||||||
|
|
||||||
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
||||||
export type HealthAppState = AppSectionState<Health>;
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
export type TaskAppState = AppSectionState<Task>;
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
diskSpace: DiskSpaceAppState;
|
diskSpace: DiskSpaceAppState;
|
||||||
health: HealthAppState;
|
health: HealthAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
tasks: TaskAppState;
|
tasks: TaskAppState;
|
||||||
|
updates: UpdateAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SystemAppState;
|
export default SystemAppState;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function createMapStateToProps() {
|
|||||||
return {
|
return {
|
||||||
...collection,
|
...collection,
|
||||||
movies: [...collection.movies].sort((a, b) => b.year - a.year),
|
movies: [...collection.movies].sort((a, b) => b.year - a.year),
|
||||||
genres: Array.from(new Set(allGenres)).slice(0, 3)
|
genres: Array.from(new Set(allGenres))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Label from 'Components/Label';
|
|||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import { icons, sizes } from 'Helpers/Props';
|
import { icons, sizes } from 'Helpers/Props';
|
||||||
|
import MovieGenres from 'Movie/MovieGenres';
|
||||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import fonts from 'Styles/Variables/fonts';
|
import fonts from 'Styles/Variables/fonts';
|
||||||
@@ -242,12 +243,10 @@ class CollectionOverview extends Component {
|
|||||||
size={sizes.MEDIUM}
|
size={sizes.MEDIUM}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.PROFILE}
|
name={icons.GENRE}
|
||||||
size={13}
|
size={13}
|
||||||
/>
|
/>
|
||||||
<span className={styles.genres}>
|
<MovieGenres className={styles.genres} genres={genres} />
|
||||||
{genres.join(', ')}
|
|
||||||
</span>
|
|
||||||
</Label>
|
</Label>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './Alert.css';
|
|
||||||
|
|
||||||
function Alert(props) {
|
|
||||||
const { className, kind, children, ...otherProps } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
styles[kind]
|
|
||||||
)}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Alert.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
|
||||||
children: PropTypes.node.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Alert.defaultProps = {
|
|
||||||
className: styles.alert,
|
|
||||||
kind: kinds.INFO
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Alert;
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import { Kind } from 'Helpers/Props/kinds';
|
||||||
|
import styles from './Alert.css';
|
||||||
|
|
||||||
|
interface AlertProps {
|
||||||
|
className?: string;
|
||||||
|
kind?: Extract<Kind, keyof typeof styles>;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Alert(props: AlertProps) {
|
||||||
|
const { className = styles.alert, kind = 'info', children } = props;
|
||||||
|
|
||||||
|
return <div className={classNames(className, styles[kind])}>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Alert;
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import styles from './Card.css';
|
|
||||||
|
|
||||||
class Card extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
overlayClassName,
|
|
||||||
overlayContent,
|
|
||||||
children,
|
|
||||||
onPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (overlayContent) {
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
onPress={onPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={overlayClassName}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
className={className}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Card.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
overlayClassName: PropTypes.string.isRequired,
|
|
||||||
overlayContent: PropTypes.bool.isRequired,
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
Card.defaultProps = {
|
|
||||||
className: styles.card,
|
|
||||||
overlayClassName: styles.overlay,
|
|
||||||
overlayContent: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Card;
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Link, { LinkProps } from 'Components/Link/Link';
|
||||||
|
import styles from './Card.css';
|
||||||
|
|
||||||
|
interface CardProps extends Pick<LinkProps, 'onPress'> {
|
||||||
|
// TODO: Consider using different properties for classname depending if it's overlaying content or not
|
||||||
|
className?: string;
|
||||||
|
overlayClassName?: string;
|
||||||
|
overlayContent?: boolean;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Card(props: CardProps) {
|
||||||
|
const {
|
||||||
|
className = styles.card,
|
||||||
|
overlayClassName = styles.overlay,
|
||||||
|
overlayContent = false,
|
||||||
|
children,
|
||||||
|
onPress,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (overlayContent) {
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<Link className={styles.underlay} onPress={onPress} />
|
||||||
|
|
||||||
|
<div className={overlayClassName}>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link className={className} onPress={onPress}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import styles from './DescriptionList.css';
|
|
||||||
|
|
||||||
class DescriptionList extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dl className={className}>
|
|
||||||
{children}
|
|
||||||
</dl>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionList.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
DescriptionList.defaultProps = {
|
|
||||||
className: styles.descriptionList
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionList;
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './DescriptionList.css';
|
||||||
|
|
||||||
|
interface DescriptionListProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionList(props: DescriptionListProps) {
|
||||||
|
const { className = styles.descriptionList, children } = props;
|
||||||
|
|
||||||
|
return <dl className={className}>{children}</dl>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionList;
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import DescriptionListItemDescription from './DescriptionListItemDescription';
|
|
||||||
import DescriptionListItemTitle from './DescriptionListItemTitle';
|
|
||||||
|
|
||||||
class DescriptionListItem extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
titleClassName,
|
|
||||||
descriptionClassName,
|
|
||||||
title,
|
|
||||||
data
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
<DescriptionListItemTitle
|
|
||||||
className={titleClassName}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</DescriptionListItemTitle>
|
|
||||||
|
|
||||||
<DescriptionListItemDescription
|
|
||||||
className={descriptionClassName}
|
|
||||||
>
|
|
||||||
{data}
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionListItem.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
titleClassName: PropTypes.string,
|
|
||||||
descriptionClassName: PropTypes.string,
|
|
||||||
title: PropTypes.string,
|
|
||||||
data: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node])
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionListItem;
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionListItemDescription, {
|
||||||
|
DescriptionListItemDescriptionProps,
|
||||||
|
} from './DescriptionListItemDescription';
|
||||||
|
import DescriptionListItemTitle, {
|
||||||
|
DescriptionListItemTitleProps,
|
||||||
|
} from './DescriptionListItemTitle';
|
||||||
|
|
||||||
|
interface DescriptionListItemProps {
|
||||||
|
className?: string;
|
||||||
|
titleClassName?: DescriptionListItemTitleProps['className'];
|
||||||
|
descriptionClassName?: DescriptionListItemDescriptionProps['className'];
|
||||||
|
title?: DescriptionListItemTitleProps['children'];
|
||||||
|
data?: DescriptionListItemDescriptionProps['children'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionListItem(props: DescriptionListItemProps) {
|
||||||
|
const { className, titleClassName, descriptionClassName, title, data } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<DescriptionListItemTitle className={titleClassName}>
|
||||||
|
{title}
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription className={descriptionClassName}>
|
||||||
|
{data}
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionListItem;
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './DescriptionListItemDescription.css';
|
|
||||||
|
|
||||||
function DescriptionListItemDescription(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dd className={className}>
|
|
||||||
{children}
|
|
||||||
</dd>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionListItemDescription.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node])
|
|
||||||
};
|
|
||||||
|
|
||||||
DescriptionListItemDescription.defaultProps = {
|
|
||||||
className: styles.description
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionListItemDescription;
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import styles from './DescriptionListItemDescription.css';
|
||||||
|
|
||||||
|
export interface DescriptionListItemDescriptionProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionListItemDescription(
|
||||||
|
props: DescriptionListItemDescriptionProps
|
||||||
|
) {
|
||||||
|
const { className = styles.description, children } = props;
|
||||||
|
|
||||||
|
return <dd className={className}>{children}</dd>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionListItemDescription;
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './DescriptionListItemTitle.css';
|
|
||||||
|
|
||||||
function DescriptionListItemTitle(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
children
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<dt className={className}>
|
|
||||||
{children}
|
|
||||||
</dt>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DescriptionListItemTitle.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
children: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
DescriptionListItemTitle.defaultProps = {
|
|
||||||
className: styles.title
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DescriptionListItemTitle;
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
import styles from './DescriptionListItemTitle.css';
|
||||||
|
|
||||||
|
export interface DescriptionListItemTitleProps {
|
||||||
|
className?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DescriptionListItemTitle(props: DescriptionListItemTitleProps) {
|
||||||
|
const { className = styles.title, children } = props;
|
||||||
|
|
||||||
|
return <dt className={className}>{children}</dt>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DescriptionListItemTitle;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import styles from './DragPreviewLayer.css';
|
|
||||||
|
|
||||||
function DragPreviewLayer({ children, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<div {...otherProps}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DragPreviewLayer.propTypes = {
|
|
||||||
children: PropTypes.node,
|
|
||||||
className: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
DragPreviewLayer.defaultProps = {
|
|
||||||
className: styles.dragLayer
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DragPreviewLayer;
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from './DragPreviewLayer.css';
|
||||||
|
|
||||||
|
interface DragPreviewLayerProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DragPreviewLayer({
|
||||||
|
className = styles.dragLayer,
|
||||||
|
children,
|
||||||
|
...otherProps
|
||||||
|
}: DragPreviewLayerProps) {
|
||||||
|
return (
|
||||||
|
<div className={className} {...otherProps}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DragPreviewLayer;
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import * as sentry from '@sentry/browser';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
|
|
||||||
class ErrorBoundary extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
error: null,
|
|
||||||
info: null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidCatch(error, info) {
|
|
||||||
this.setState({
|
|
||||||
error,
|
|
||||||
info
|
|
||||||
});
|
|
||||||
|
|
||||||
sentry.captureException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
children,
|
|
||||||
errorComponent: ErrorComponent,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
error,
|
|
||||||
info
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<ErrorComponent
|
|
||||||
error={error}
|
|
||||||
info={info}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorBoundary.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
errorComponent: PropTypes.elementType.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ErrorBoundary;
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import * as sentry from '@sentry/browser';
|
||||||
|
import React, { Component, ErrorInfo } from 'react';
|
||||||
|
|
||||||
|
interface ErrorBoundaryProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
errorComponent: React.ElementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorBoundaryState {
|
||||||
|
error: Error | null;
|
||||||
|
info: ErrorInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class component until componentDidCatch is supported in functional components
|
||||||
|
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
||||||
|
constructor(props: ErrorBoundaryProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
error: null,
|
||||||
|
info: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, info: ErrorInfo) {
|
||||||
|
this.setState({
|
||||||
|
error,
|
||||||
|
info,
|
||||||
|
});
|
||||||
|
|
||||||
|
sentry.captureException(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children, errorComponent: ErrorComponent } = this.props;
|
||||||
|
const { error, info } = this.state;
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorComponent error={error} info={info} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import styles from './FieldSet.css';
|
|
||||||
|
|
||||||
class FieldSet extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
size,
|
|
||||||
legend,
|
|
||||||
children
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<fieldset className={styles.fieldSet}>
|
|
||||||
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
|
|
||||||
{legend}
|
|
||||||
</legend>
|
|
||||||
{children}
|
|
||||||
</fieldset>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
FieldSet.propTypes = {
|
|
||||||
size: PropTypes.oneOf(sizes.all).isRequired,
|
|
||||||
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
|
||||||
children: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
FieldSet.defaultProps = {
|
|
||||||
size: sizes.MEDIUM
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FieldSet;
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { ComponentProps } from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { Size } from 'Helpers/Props/sizes';
|
||||||
|
import styles from './FieldSet.css';
|
||||||
|
|
||||||
|
interface FieldSetProps {
|
||||||
|
size?: Size;
|
||||||
|
legend?: ComponentProps<'legend'>['children'];
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FieldSet({ size = sizes.MEDIUM, legend, children }: FieldSetProps) {
|
||||||
|
return (
|
||||||
|
<fieldset className={styles.fieldSet}>
|
||||||
|
<legend
|
||||||
|
className={classNames(
|
||||||
|
styles.legend,
|
||||||
|
size === sizes.SMALL && styles.small
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{legend}
|
||||||
|
</legend>
|
||||||
|
{children}
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FieldSet;
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import FileBrowserModalContentConnector from './FileBrowserModalContentConnector';
|
|
||||||
import styles from './FileBrowserModal.css';
|
|
||||||
|
|
||||||
class FileBrowserModal extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className={styles.modal}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<FileBrowserModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileBrowserModal;
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import FileBrowserModalContent, {
|
||||||
|
FileBrowserModalContentProps,
|
||||||
|
} from './FileBrowserModalContent';
|
||||||
|
import styles from './FileBrowserModal.css';
|
||||||
|
|
||||||
|
interface FileBrowserModalProps extends FileBrowserModalContentProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileBrowserModal(props: FileBrowserModalProps) {
|
||||||
|
const { isOpen, onModalClose, ...otherProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal className={styles.modal} isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
|
<FileBrowserModalContent {...otherProps} onModalClose={onModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileBrowserModal;
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import PathInput from 'Components/Form/PathInput';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import Scroller from 'Components/Scroller/Scroller';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import FileBrowserRow from './FileBrowserRow';
|
|
||||||
import styles from './FileBrowserModalContent.css';
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
name: 'type',
|
|
||||||
label: () => translate('Type'),
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
label: () => translate('Name'),
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
class FileBrowserModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._scrollerRef = React.createRef();
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isFileBrowserModalOpen: false,
|
|
||||||
currentPath: props.value
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
const {
|
|
||||||
currentPath
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (
|
|
||||||
currentPath !== this.state.currentPath &&
|
|
||||||
currentPath !== prevState.currentPath
|
|
||||||
) {
|
|
||||||
this.setState({ currentPath });
|
|
||||||
this._scrollerRef.current.scrollTop = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPathInputChange = ({ value }) => {
|
|
||||||
this.setState({ currentPath: value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRowPress = (path) => {
|
|
||||||
this.props.onFetchPaths(path);
|
|
||||||
};
|
|
||||||
|
|
||||||
onOkPress = () => {
|
|
||||||
this.props.onChange({
|
|
||||||
name: this.props.name,
|
|
||||||
value: this.state.currentPath
|
|
||||||
});
|
|
||||||
|
|
||||||
this.props.onClearPaths();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
parent,
|
|
||||||
directories,
|
|
||||||
files,
|
|
||||||
isWindowsService,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const emptyParent = parent === '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalHeader>
|
|
||||||
File Browser
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody
|
|
||||||
className={styles.modalBody}
|
|
||||||
scrollDirection={scrollDirections.NONE}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isWindowsService &&
|
|
||||||
<Alert
|
|
||||||
className={styles.mappedDrivesWarning}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
>
|
|
||||||
<Link to="https://wiki.servarr.com/radarr/faq#why-cant-radarr-see-my-files-on-a-remote-server">
|
|
||||||
{translate('MappedDrivesRunningAsService')}
|
|
||||||
</Link> .
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
<PathInput
|
|
||||||
className={styles.pathInput}
|
|
||||||
placeholder={translate('StartTypingOrSelectAPathBelow')}
|
|
||||||
hasFileBrowser={false}
|
|
||||||
{...otherProps}
|
|
||||||
value={this.state.currentPath}
|
|
||||||
onChange={this.onPathInputChange}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Scroller
|
|
||||||
ref={this._scrollerRef}
|
|
||||||
className={styles.scroller}
|
|
||||||
scrollDirection={scrollDirections.BOTH}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
!!error &&
|
|
||||||
<div>
|
|
||||||
{translate('ErrorLoadingContents')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error &&
|
|
||||||
<Table
|
|
||||||
horizontalScroll={false}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
emptyParent &&
|
|
||||||
<FileBrowserRow
|
|
||||||
type="computer"
|
|
||||||
name="My Computer"
|
|
||||||
path={parent}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!emptyParent && parent &&
|
|
||||||
<FileBrowserRow
|
|
||||||
type="parent"
|
|
||||||
name="..."
|
|
||||||
path={parent}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
directories.map((directory) => {
|
|
||||||
return (
|
|
||||||
<FileBrowserRow
|
|
||||||
key={directory.path}
|
|
||||||
type={directory.type}
|
|
||||||
name={directory.name}
|
|
||||||
path={directory.path}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
files.map((file) => {
|
|
||||||
return (
|
|
||||||
<FileBrowserRow
|
|
||||||
key={file.path}
|
|
||||||
type={file.type}
|
|
||||||
name={file.name}
|
|
||||||
path={file.path}
|
|
||||||
onPress={this.onRowPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
}
|
|
||||||
</Scroller>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
{
|
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator
|
|
||||||
className={styles.loading}
|
|
||||||
size={20}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
onPress={this.onOkPress}
|
|
||||||
>
|
|
||||||
{translate('Ok')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModalContent.propTypes = {
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.string.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
parent: PropTypes.string,
|
|
||||||
currentPath: PropTypes.string.isRequired,
|
|
||||||
directories: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
files: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isWindowsService: PropTypes.bool.isRequired,
|
|
||||||
onFetchPaths: PropTypes.func.isRequired,
|
|
||||||
onClearPaths: PropTypes.func.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileBrowserModalContent;
|
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import PathInput from 'Components/Form/PathInput';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import Scroller from 'Components/Scroller/Scroller';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||||
|
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import createPathsSelector from './createPathsSelector';
|
||||||
|
import FileBrowserRow from './FileBrowserRow';
|
||||||
|
import styles from './FileBrowserModalContent.css';
|
||||||
|
|
||||||
|
const columns: Column[] = [
|
||||||
|
{
|
||||||
|
name: 'type',
|
||||||
|
label: () => translate('Type'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
label: () => translate('Name'),
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleClearPaths = () => {};
|
||||||
|
|
||||||
|
export interface FileBrowserModalContentProps {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
includeFiles?: boolean;
|
||||||
|
onChange: (args: InputChanged<string>) => unknown;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileBrowserModalContent(props: FileBrowserModalContentProps) {
|
||||||
|
const { name, value, includeFiles = true, onChange, onModalClose } = props;
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { isWindows, mode } = useSelector(createSystemStatusSelector());
|
||||||
|
const { isFetching, isPopulated, error, parent, directories, files, paths } =
|
||||||
|
useSelector(createPathsSelector());
|
||||||
|
|
||||||
|
const [currentPath, setCurrentPath] = useState(value);
|
||||||
|
const scrollerRef = useRef(null);
|
||||||
|
const previousValue = usePrevious(value);
|
||||||
|
|
||||||
|
const emptyParent = parent === '';
|
||||||
|
const isWindowsService = isWindows && mode === 'service';
|
||||||
|
|
||||||
|
const handlePathInputChange = useCallback(
|
||||||
|
({ value }: InputChanged<string>) => {
|
||||||
|
setCurrentPath(value);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowPress = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
setCurrentPath(path);
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
fetchPaths({
|
||||||
|
path,
|
||||||
|
allowFoldersWithoutTrailingSlashes: true,
|
||||||
|
includeFiles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch, setCurrentPath]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOkPress = useCallback(() => {
|
||||||
|
onChange({
|
||||||
|
name,
|
||||||
|
value: currentPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
dispatch(clearPaths());
|
||||||
|
onModalClose();
|
||||||
|
}, [name, currentPath, dispatch, onChange, onModalClose]);
|
||||||
|
|
||||||
|
const handleFetchPaths = useCallback(
|
||||||
|
(path: string) => {
|
||||||
|
dispatch(
|
||||||
|
fetchPaths({
|
||||||
|
path,
|
||||||
|
allowFoldersWithoutTrailingSlashes: true,
|
||||||
|
includeFiles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[includeFiles, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value !== previousValue && value !== currentPath) {
|
||||||
|
setCurrentPath(value);
|
||||||
|
}
|
||||||
|
}, [value, previousValue, currentPath, setCurrentPath]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
dispatch(
|
||||||
|
fetchPaths({
|
||||||
|
path: currentPath,
|
||||||
|
allowFoldersWithoutTrailingSlashes: true,
|
||||||
|
includeFiles,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearPaths());
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// This should only run once when the component mounts,
|
||||||
|
// so we don't need to include the other dependencies.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('FileBrowser')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody
|
||||||
|
className={styles.modalBody}
|
||||||
|
scrollDirection={scrollDirections.NONE}
|
||||||
|
>
|
||||||
|
{isWindowsService ? (
|
||||||
|
<Alert className={styles.mappedDrivesWarning} kind={kinds.WARNING}>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('MappedNetworkDrivesWindowsService', {
|
||||||
|
url: 'https://wiki.servarr.com/radarr/faq#why-cant-radarr-see-my-files-on-a-remote-server',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<PathInput
|
||||||
|
className={styles.pathInput}
|
||||||
|
placeholder={translate('FileBrowserPlaceholderText')}
|
||||||
|
hasFileBrowser={false}
|
||||||
|
includeFiles={includeFiles}
|
||||||
|
paths={paths}
|
||||||
|
name={name}
|
||||||
|
value={currentPath}
|
||||||
|
onChange={handlePathInputChange}
|
||||||
|
onFetchPaths={handleFetchPaths}
|
||||||
|
onClearPaths={handleClearPaths}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Scroller
|
||||||
|
ref={scrollerRef}
|
||||||
|
className={styles.scroller}
|
||||||
|
scrollDirection="both"
|
||||||
|
>
|
||||||
|
{error ? <div>{translate('ErrorLoadingContents')}</div> : null}
|
||||||
|
|
||||||
|
{isPopulated && !error ? (
|
||||||
|
<Table horizontalScroll={false} columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{emptyParent ? (
|
||||||
|
<FileBrowserRow
|
||||||
|
type="computer"
|
||||||
|
name={translate('MyComputer')}
|
||||||
|
path={parent}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!emptyParent && parent ? (
|
||||||
|
<FileBrowserRow
|
||||||
|
type="parent"
|
||||||
|
name="..."
|
||||||
|
path={parent}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{directories.map((directory) => {
|
||||||
|
return (
|
||||||
|
<FileBrowserRow
|
||||||
|
key={directory.path}
|
||||||
|
type={directory.type}
|
||||||
|
name={directory.name}
|
||||||
|
path={directory.path}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{files.map((file) => {
|
||||||
|
return (
|
||||||
|
<FileBrowserRow
|
||||||
|
key={file.path}
|
||||||
|
type={file.type}
|
||||||
|
name={file.name}
|
||||||
|
path={file.path}
|
||||||
|
onPress={handleRowPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
) : null}
|
||||||
|
</Scroller>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
{isFetching ? (
|
||||||
|
<LoadingIndicator className={styles.loading} size={20} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<Button onPress={handleOkPress}>{translate('Ok')}</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileBrowserModalContent;
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { clearPaths, fetchPaths } from 'Store/Actions/pathActions';
|
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
|
||||||
import FileBrowserModalContent from './FileBrowserModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.paths,
|
|
||||||
createSystemStatusSelector(),
|
|
||||||
(paths, systemStatus) => {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
parent,
|
|
||||||
currentPath,
|
|
||||||
directories,
|
|
||||||
files
|
|
||||||
} = paths;
|
|
||||||
|
|
||||||
const filteredPaths = _.filter([...directories, ...files], ({ path }) => {
|
|
||||||
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
parent,
|
|
||||||
currentPath,
|
|
||||||
directories,
|
|
||||||
files,
|
|
||||||
paths: filteredPaths,
|
|
||||||
isWindowsService: systemStatus.isWindows && systemStatus.mode === 'service'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchFetchPaths: fetchPaths,
|
|
||||||
dispatchClearPaths: clearPaths
|
|
||||||
};
|
|
||||||
|
|
||||||
class FileBrowserModalContentConnector extends Component {
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
value,
|
|
||||||
includeFiles,
|
|
||||||
dispatchFetchPaths
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchPaths({
|
|
||||||
path: value,
|
|
||||||
allowFoldersWithoutTrailingSlashes: true,
|
|
||||||
includeFiles
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFetchPaths = (path) => {
|
|
||||||
const {
|
|
||||||
includeFiles,
|
|
||||||
dispatchFetchPaths
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
dispatchFetchPaths({
|
|
||||||
path,
|
|
||||||
allowFoldersWithoutTrailingSlashes: true,
|
|
||||||
includeFiles
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onClearPaths = () => {
|
|
||||||
// this.props.dispatchClearPaths();
|
|
||||||
};
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.dispatchClearPaths();
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<FileBrowserModalContent
|
|
||||||
onFetchPaths={this.onFetchPaths}
|
|
||||||
onClearPaths={this.onClearPaths}
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserModalContentConnector.propTypes = {
|
|
||||||
value: PropTypes.string,
|
|
||||||
includeFiles: PropTypes.bool.isRequired,
|
|
||||||
dispatchFetchPaths: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPaths: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FileBrowserModalContentConnector.defaultProps = {
|
|
||||||
includeFiles: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector);
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRowButton from 'Components/Table/TableRowButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import styles from './FileBrowserRow.css';
|
|
||||||
|
|
||||||
function getIconName(type) {
|
|
||||||
switch (type) {
|
|
||||||
case 'computer':
|
|
||||||
return icons.COMPUTER;
|
|
||||||
case 'drive':
|
|
||||||
return icons.DRIVE;
|
|
||||||
case 'file':
|
|
||||||
return icons.FILE;
|
|
||||||
case 'parent':
|
|
||||||
return icons.PARENT;
|
|
||||||
default:
|
|
||||||
return icons.FOLDER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FileBrowserRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.props.onPress(this.props.path);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
name
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowButton onPress={this.onPress}>
|
|
||||||
<TableRowCell className={styles.type}>
|
|
||||||
<Icon name={getIconName(type)} />
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell>{name}</TableRowCell>
|
|
||||||
</TableRowButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
FileBrowserRow.propTypes = {
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
path: PropTypes.string.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileBrowserRow;
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { PathType } from 'App/State/PathsAppState';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import styles from './FileBrowserRow.css';
|
||||||
|
|
||||||
|
function getIconName(type: PathType) {
|
||||||
|
switch (type) {
|
||||||
|
case 'computer':
|
||||||
|
return icons.COMPUTER;
|
||||||
|
case 'drive':
|
||||||
|
return icons.DRIVE;
|
||||||
|
case 'file':
|
||||||
|
return icons.FILE;
|
||||||
|
case 'parent':
|
||||||
|
return icons.PARENT;
|
||||||
|
default:
|
||||||
|
return icons.FOLDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FileBrowserRowProps {
|
||||||
|
type: PathType;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
onPress: (path: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileBrowserRow(props: FileBrowserRowProps) {
|
||||||
|
const { type, name, path, onPress } = props;
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
onPress(path);
|
||||||
|
}, [path, onPress]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowButton onPress={handlePress}>
|
||||||
|
<TableRowCell className={styles.type}>
|
||||||
|
<Icon name={getIconName(type)} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell>{name}</TableRowCell>
|
||||||
|
</TableRowButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileBrowserRow;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createPathsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.paths,
|
||||||
|
(paths) => {
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
parent,
|
||||||
|
currentPath,
|
||||||
|
directories,
|
||||||
|
files,
|
||||||
|
} = paths;
|
||||||
|
|
||||||
|
const filteredPaths = [...directories, ...files].filter(({ path }) => {
|
||||||
|
return path.toLowerCase().startsWith(currentPath.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
parent,
|
||||||
|
currentPath,
|
||||||
|
directories,
|
||||||
|
files,
|
||||||
|
paths: filteredPaths,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createPathsSelector;
|
||||||
@@ -16,6 +16,7 @@ import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
|
|||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
|
||||||
|
import QueueStatusFilterBuilderRowValue from './QueueStatusFilterBuilderRowValue';
|
||||||
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
|
import ReleaseStatusFilterBuilderRowValue from './ReleaseStatusFilterBuilderRowValue';
|
||||||
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
|
||||||
import styles from './FilterBuilderRow.css';
|
import styles from './FilterBuilderRow.css';
|
||||||
@@ -80,6 +81,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValue;
|
return QualityProfileFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.QUEUE_STATUS:
|
||||||
|
return QueueStatusFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.MOVIE:
|
case filterBuilderValueTypes.MOVIE:
|
||||||
return MovieFilterBuilderRowValue;
|
return MovieFilterBuilderRowValue;
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function getValue(input, selectedFilterBuilderProp) {
|
|||||||
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
|
||||||
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
|
const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
|
||||||
|
|
||||||
return Number(input).toFixed(numberFractionDigits);
|
return Number(Number(input).toFixed(numberFractionDigits));
|
||||||
}
|
}
|
||||||
|
|
||||||
return input;
|
return input;
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
const statusTagList = [
|
||||||
|
{
|
||||||
|
id: 'queued',
|
||||||
|
get name() {
|
||||||
|
return translate('Queued');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'paused',
|
||||||
|
get name() {
|
||||||
|
return translate('Paused');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'downloading',
|
||||||
|
get name() {
|
||||||
|
return translate('Downloading');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'completed',
|
||||||
|
get name() {
|
||||||
|
return translate('Completed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'failed',
|
||||||
|
get name() {
|
||||||
|
return translate('Failed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'warning',
|
||||||
|
get name() {
|
||||||
|
return translate('Warning');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'delay',
|
||||||
|
get name() {
|
||||||
|
return translate('Delay');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'downloadClientUnavailable',
|
||||||
|
get name() {
|
||||||
|
return translate('DownloadClientUnavailable');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fallback',
|
||||||
|
get name() {
|
||||||
|
return translate('Fallback');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function QueueStatusFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={statusTagList} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default QueueStatusFilterBuilderRowValue;
|
||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
|
||||||
const protocols = [
|
const statusTagList = [
|
||||||
{ id: 'tba', name: 'TBA' },
|
{ id: 'tba', name: 'TBA' },
|
||||||
{
|
{
|
||||||
id: 'announced',
|
id: 'announced',
|
||||||
@@ -33,7 +33,7 @@ const protocols = [
|
|||||||
function ReleaseStatusFilterBuilderRowValue(props) {
|
function ReleaseStatusFilterBuilderRowValue(props) {
|
||||||
return (
|
return (
|
||||||
<FilterBuilderRowValue
|
<FilterBuilderRowValue
|
||||||
tagList={protocols}
|
tagList={statusTagList}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
|
||||||
|
|
||||||
const availabilityOptions = [
|
|
||||||
{
|
|
||||||
key: 'announced',
|
|
||||||
get value() {
|
|
||||||
return translate('Announced');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'inCinemas',
|
|
||||||
get value() {
|
|
||||||
return translate('InCinemas');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'released',
|
|
||||||
get value() {
|
|
||||||
return translate('Released');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function AvailabilitySelectInput(props) {
|
|
||||||
const values = [...availabilityOptions];
|
|
||||||
|
|
||||||
const {
|
|
||||||
includeNoChange,
|
|
||||||
includeMixed
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (includeNoChange) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'noChange',
|
|
||||||
value: translate('NoChange'),
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (includeMixed) {
|
|
||||||
values.unshift({
|
|
||||||
key: 'mixed',
|
|
||||||
value: '(Mixed)',
|
|
||||||
isDisabled: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnhancedSelectInput
|
|
||||||
{...props}
|
|
||||||
values={values}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AvailabilitySelectInput.propTypes = {
|
|
||||||
includeNoChange: PropTypes.bool.isRequired,
|
|
||||||
includeMixed: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
AvailabilitySelectInput.defaultProps = {
|
|
||||||
includeNoChange: false,
|
|
||||||
includeMixed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AvailabilitySelectInput;
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
interface AvailabilitySelectInputProps {
|
||||||
|
includeNoChange: boolean;
|
||||||
|
includeNoChangeDisabled?: boolean;
|
||||||
|
includeMixed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMovieAvailabilityOption {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
format?: string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const movieAvailabilityOptions: IMovieAvailabilityOption[] = [
|
||||||
|
{
|
||||||
|
key: 'announced',
|
||||||
|
get value() {
|
||||||
|
return translate('Announced');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'inCinemas',
|
||||||
|
get value() {
|
||||||
|
return translate('InCinemas');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'released',
|
||||||
|
get value() {
|
||||||
|
return translate('Released');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function AvailabilitySelectInput(props: AvailabilitySelectInputProps) {
|
||||||
|
const values = [...movieAvailabilityOptions];
|
||||||
|
|
||||||
|
const {
|
||||||
|
includeNoChange = false,
|
||||||
|
includeNoChangeDisabled = true,
|
||||||
|
includeMixed = false,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
if (includeNoChange) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'noChange',
|
||||||
|
value: translate('NoChange'),
|
||||||
|
isDisabled: includeNoChangeDisabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeMixed) {
|
||||||
|
values.unshift({
|
||||||
|
key: 'mixed',
|
||||||
|
value: `(${translate('Mixed')})`,
|
||||||
|
isDisabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return <EnhancedSelectInput {...props} values={values} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AvailabilitySelectInput;
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import styles from './FormInputButton.css';
|
|
||||||
|
|
||||||
function FormInputButton(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
canSpin,
|
|
||||||
isLastButton,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (canSpin) {
|
|
||||||
return (
|
|
||||||
<SpinnerButton
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
className={classNames(
|
|
||||||
className,
|
|
||||||
!isLastButton && styles.middleButton
|
|
||||||
)}
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
FormInputButton.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
isLastButton: PropTypes.bool.isRequired,
|
|
||||||
canSpin: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
FormInputButton.defaultProps = {
|
|
||||||
className: styles.button,
|
|
||||||
isLastButton: true,
|
|
||||||
canSpin: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FormInputButton;
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
import Button, { ButtonProps } from 'Components/Link/Button';
|
||||||
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import styles from './FormInputButton.css';
|
||||||
|
|
||||||
|
export interface FormInputButtonProps extends ButtonProps {
|
||||||
|
canSpin?: boolean;
|
||||||
|
isLastButton?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormInputButton({
|
||||||
|
className = styles.button,
|
||||||
|
canSpin = false,
|
||||||
|
isLastButton = true,
|
||||||
|
...otherProps
|
||||||
|
}: FormInputButtonProps) {
|
||||||
|
if (canSpin) {
|
||||||
|
return (
|
||||||
|
<SpinnerButton
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={classNames(className, !isLastButton && styles.middleButton)}
|
||||||
|
kind={kinds.PRIMARY}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FormInputButton;
|
||||||
@@ -272,6 +272,8 @@ FormInputGroup.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
values: PropTypes.arrayOf(PropTypes.any),
|
values: PropTypes.arrayOf(PropTypes.any),
|
||||||
|
placeholder: PropTypes.string,
|
||||||
|
delimiters: PropTypes.arrayOf(PropTypes.string),
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
kind: PropTypes.oneOf(kinds.all),
|
kind: PropTypes.oneOf(kinds.all),
|
||||||
@@ -284,8 +286,10 @@ FormInputGroup.propTypes = {
|
|||||||
helpTextWarning: PropTypes.string,
|
helpTextWarning: PropTypes.string,
|
||||||
helpLink: PropTypes.string,
|
helpLink: PropTypes.string,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
|
canEdit: PropTypes.bool,
|
||||||
includeNoChange: PropTypes.bool,
|
includeNoChange: PropTypes.bool,
|
||||||
includeNoChangeDisabled: PropTypes.bool,
|
includeNoChangeDisabled: PropTypes.bool,
|
||||||
|
includeAny: PropTypes.bool,
|
||||||
selectedValueOptions: PropTypes.object,
|
selectedValueOptions: PropTypes.object,
|
||||||
indexerFlags: PropTypes.number,
|
indexerFlags: PropTypes.number,
|
||||||
pending: PropTypes.bool,
|
pending: PropTypes.bool,
|
||||||
|
|||||||
@@ -1,156 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import KeyValueListInputItem from './KeyValueListInputItem';
|
|
||||||
import styles from './KeyValueListInput.css';
|
|
||||||
|
|
||||||
class KeyValueListInput extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isFocused: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onItemChange = (index, itemValue) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = [...value];
|
|
||||||
|
|
||||||
if (index == null) {
|
|
||||||
newValue.push(itemValue);
|
|
||||||
} else {
|
|
||||||
newValue.splice(index, 1, itemValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemoveItem = (index) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = [...value];
|
|
||||||
newValue.splice(index, 1);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.setState({
|
|
||||||
isFocused: true
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
this.setState({
|
|
||||||
isFocused: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const newValue = value.reduce((acc, v) => {
|
|
||||||
if (v.key || v.value) {
|
|
||||||
acc.push(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (newValue.length !== value.length) {
|
|
||||||
onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
value,
|
|
||||||
keyPlaceholder,
|
|
||||||
valuePlaceholder,
|
|
||||||
hasError,
|
|
||||||
hasWarning
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const { isFocused } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
className,
|
|
||||||
isFocused && styles.isFocused,
|
|
||||||
hasError && styles.hasError,
|
|
||||||
hasWarning && styles.hasWarning
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
[...value, { key: '', value: '' }].map((v, index) => {
|
|
||||||
return (
|
|
||||||
<KeyValueListInputItem
|
|
||||||
key={index}
|
|
||||||
index={index}
|
|
||||||
keyValue={v.key}
|
|
||||||
value={v.value}
|
|
||||||
keyPlaceholder={keyPlaceholder}
|
|
||||||
valuePlaceholder={valuePlaceholder}
|
|
||||||
isNew={index === value.length}
|
|
||||||
onChange={this.onItemChange}
|
|
||||||
onRemove={this.onRemoveItem}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyValueListInput.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hasError: PropTypes.bool,
|
|
||||||
hasWarning: PropTypes.bool,
|
|
||||||
keyPlaceholder: PropTypes.string,
|
|
||||||
valuePlaceholder: PropTypes.string,
|
|
||||||
onChange: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
KeyValueListInput.defaultProps = {
|
|
||||||
className: styles.inputContainer,
|
|
||||||
value: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KeyValueListInput;
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { InputOnChange } from 'typings/inputs';
|
||||||
|
import KeyValueListInputItem from './KeyValueListInputItem';
|
||||||
|
import styles from './KeyValueListInput.css';
|
||||||
|
|
||||||
|
interface KeyValue {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KeyValueListInputProps {
|
||||||
|
className?: string;
|
||||||
|
name: string;
|
||||||
|
value: KeyValue[];
|
||||||
|
hasError?: boolean;
|
||||||
|
hasWarning?: boolean;
|
||||||
|
keyPlaceholder?: string;
|
||||||
|
valuePlaceholder?: string;
|
||||||
|
onChange: InputOnChange<KeyValue[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function KeyValueListInput({
|
||||||
|
className = styles.inputContainer,
|
||||||
|
name,
|
||||||
|
value = [],
|
||||||
|
hasError = false,
|
||||||
|
hasWarning = false,
|
||||||
|
keyPlaceholder,
|
||||||
|
valuePlaceholder,
|
||||||
|
onChange,
|
||||||
|
}: KeyValueListInputProps): JSX.Element {
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
const handleItemChange = useCallback(
|
||||||
|
(index: number | null, itemValue: KeyValue) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
|
||||||
|
if (index === null) {
|
||||||
|
newValue.push(itemValue);
|
||||||
|
} else {
|
||||||
|
newValue.splice(index, 1, itemValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange({ name, value: newValue });
|
||||||
|
},
|
||||||
|
[value, name, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemoveItem = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const newValue = [...value];
|
||||||
|
newValue.splice(index, 1);
|
||||||
|
onChange({ name, value: newValue });
|
||||||
|
},
|
||||||
|
[value, name, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFocus = useCallback(() => setIsFocused(true), []);
|
||||||
|
|
||||||
|
const onBlur = useCallback(() => {
|
||||||
|
setIsFocused(false);
|
||||||
|
|
||||||
|
const newValue = value.reduce((acc: KeyValue[], v) => {
|
||||||
|
if (v.key || v.value) {
|
||||||
|
acc.push(v);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (newValue.length !== value.length) {
|
||||||
|
onChange({ name, value: newValue });
|
||||||
|
}
|
||||||
|
}, [value, name, onChange]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
className,
|
||||||
|
isFocused && styles.isFocused,
|
||||||
|
hasError && styles.hasError,
|
||||||
|
hasWarning && styles.hasWarning
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[...value, { key: '', value: '' }].map((v, index) => (
|
||||||
|
<KeyValueListInputItem
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
keyValue={v.key}
|
||||||
|
value={v.value}
|
||||||
|
keyPlaceholder={keyPlaceholder}
|
||||||
|
valuePlaceholder={valuePlaceholder}
|
||||||
|
isNew={index === value.length}
|
||||||
|
onChange={handleItemChange}
|
||||||
|
onRemove={handleRemoveItem}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyValueListInput;
|
||||||
@@ -5,13 +5,19 @@
|
|||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputWrapper {
|
.keyInputWrapper {
|
||||||
flex: 1 0 0;
|
flex: 1 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.valueInputWrapper {
|
||||||
|
flex: 1 0 0;
|
||||||
|
min-width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonWrapper {
|
.buttonWrapper {
|
||||||
flex: 0 0 22px;
|
flex: 0 0 22px;
|
||||||
}
|
}
|
||||||
@@ -20,4 +26,10 @@
|
|||||||
.valueInput {
|
.valueInput {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: none;
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--textColor);
|
||||||
|
|
||||||
|
&::placeholder {
|
||||||
|
color: var(--helpTextColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'buttonWrapper': string;
|
'buttonWrapper': string;
|
||||||
'inputWrapper': string;
|
|
||||||
'itemContainer': string;
|
'itemContainer': string;
|
||||||
'keyInput': string;
|
'keyInput': string;
|
||||||
|
'keyInputWrapper': string;
|
||||||
'valueInput': string;
|
'valueInput': string;
|
||||||
|
'valueInputWrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import TextInput from './TextInput';
|
|
||||||
import styles from './KeyValueListInputItem.css';
|
|
||||||
|
|
||||||
class KeyValueListInputItem extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onKeyChange = ({ value: keyValue }) => {
|
|
||||||
const {
|
|
||||||
index,
|
|
||||||
value,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onValueChange = ({ value }) => {
|
|
||||||
// TODO: Validate here or validate at a lower level component
|
|
||||||
|
|
||||||
const {
|
|
||||||
index,
|
|
||||||
keyValue,
|
|
||||||
onChange
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onChange(index, { key: keyValue, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemovePress = () => {
|
|
||||||
const {
|
|
||||||
index,
|
|
||||||
onRemove
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onRemove(index);
|
|
||||||
};
|
|
||||||
|
|
||||||
onFocus = () => {
|
|
||||||
this.props.onFocus();
|
|
||||||
};
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
this.props.onBlur();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
keyValue,
|
|
||||||
value,
|
|
||||||
keyPlaceholder,
|
|
||||||
valuePlaceholder,
|
|
||||||
isNew
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.itemContainer}>
|
|
||||||
<div className={styles.inputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.keyInput}
|
|
||||||
name="key"
|
|
||||||
value={keyValue}
|
|
||||||
placeholder={keyPlaceholder}
|
|
||||||
onChange={this.onKeyChange}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.inputWrapper}>
|
|
||||||
<TextInput
|
|
||||||
className={styles.valueInput}
|
|
||||||
name="value"
|
|
||||||
value={value}
|
|
||||||
placeholder={valuePlaceholder}
|
|
||||||
onChange={this.onValueChange}
|
|
||||||
onFocus={this.onFocus}
|
|
||||||
onBlur={this.onBlur}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.buttonWrapper}>
|
|
||||||
{
|
|
||||||
isNew ?
|
|
||||||
null :
|
|
||||||
<IconButton
|
|
||||||
name={icons.REMOVE}
|
|
||||||
tabIndex={-1}
|
|
||||||
onPress={this.onRemovePress}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyValueListInputItem.propTypes = {
|
|
||||||
index: PropTypes.number,
|
|
||||||
keyValue: PropTypes.string.isRequired,
|
|
||||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
keyPlaceholder: PropTypes.string.isRequired,
|
|
||||||
valuePlaceholder: PropTypes.string.isRequired,
|
|
||||||
isNew: PropTypes.bool.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onRemove: PropTypes.func.isRequired,
|
|
||||||
onFocus: PropTypes.func.isRequired,
|
|
||||||
onBlur: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
KeyValueListInputItem.defaultProps = {
|
|
||||||
keyPlaceholder: 'Key',
|
|
||||||
valuePlaceholder: 'Value'
|
|
||||||
};
|
|
||||||
|
|
||||||
export default KeyValueListInputItem;
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import TextInput from './TextInput';
|
||||||
|
import styles from './KeyValueListInputItem.css';
|
||||||
|
|
||||||
|
interface KeyValueListInputItemProps {
|
||||||
|
index: number;
|
||||||
|
keyValue: string;
|
||||||
|
value: string;
|
||||||
|
keyPlaceholder?: string;
|
||||||
|
valuePlaceholder?: string;
|
||||||
|
isNew: boolean;
|
||||||
|
onChange: (index: number, itemValue: { key: string; value: string }) => void;
|
||||||
|
onRemove: (index: number) => void;
|
||||||
|
onFocus: () => void;
|
||||||
|
onBlur: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function KeyValueListInputItem({
|
||||||
|
index,
|
||||||
|
keyValue,
|
||||||
|
value,
|
||||||
|
keyPlaceholder = 'Key',
|
||||||
|
valuePlaceholder = 'Value',
|
||||||
|
isNew,
|
||||||
|
onChange,
|
||||||
|
onRemove,
|
||||||
|
onFocus,
|
||||||
|
onBlur,
|
||||||
|
}: KeyValueListInputItemProps): JSX.Element {
|
||||||
|
const handleKeyChange = useCallback(
|
||||||
|
({ value: keyValue }: { value: string }) => {
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
},
|
||||||
|
[index, value, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleValueChange = useCallback(
|
||||||
|
({ value }: { value: string }) => {
|
||||||
|
onChange(index, { key: keyValue, value });
|
||||||
|
},
|
||||||
|
[index, keyValue, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemovePress = useCallback(() => {
|
||||||
|
onRemove(index);
|
||||||
|
}, [index, onRemove]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.itemContainer}>
|
||||||
|
<div className={styles.keyInputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.keyInput}
|
||||||
|
name="key"
|
||||||
|
value={keyValue}
|
||||||
|
placeholder={keyPlaceholder}
|
||||||
|
onChange={handleKeyChange}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.valueInputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
className={styles.valueInput}
|
||||||
|
name="value"
|
||||||
|
value={value}
|
||||||
|
placeholder={valuePlaceholder}
|
||||||
|
onChange={handleValueChange}
|
||||||
|
onFocus={onFocus}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonWrapper}>
|
||||||
|
{isNew ? null : (
|
||||||
|
<IconButton
|
||||||
|
name={icons.REMOVE}
|
||||||
|
tabIndex={-1}
|
||||||
|
onPress={handleRemovePress}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default KeyValueListInputItem;
|
||||||
@@ -5,17 +5,20 @@ import translate from 'Utilities/String/translate';
|
|||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
function MovieMonitoredSelectInput(props) {
|
function MovieMonitoredSelectInput(props) {
|
||||||
const values = [...monitorOptions];
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
includeNoChange,
|
includeNoChange,
|
||||||
includeMixed
|
includeMixed,
|
||||||
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const values = [...monitorOptions];
|
||||||
|
|
||||||
if (includeNoChange) {
|
if (includeNoChange) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'noChange',
|
key: 'noChange',
|
||||||
value: translate('NoChange'),
|
get value() {
|
||||||
|
return translate('NoChange');
|
||||||
|
},
|
||||||
isDisabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
|
|||||||
if (includeMixed) {
|
if (includeMixed) {
|
||||||
values.unshift({
|
values.unshift({
|
||||||
key: 'mixed',
|
key: 'mixed',
|
||||||
value: '(Mixed)',
|
get value() {
|
||||||
|
return `(${translate('Mixed')})`;
|
||||||
|
},
|
||||||
isDisabled: true
|
isDisabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EnhancedSelectInput
|
<EnhancedSelectInput
|
||||||
{...props}
|
{...otherProps}
|
||||||
values={values}
|
values={values}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||||||
return inputTypes.CHECK;
|
return inputTypes.CHECK;
|
||||||
case 'device':
|
case 'device':
|
||||||
return inputTypes.DEVICE;
|
return inputTypes.DEVICE;
|
||||||
|
case 'keyValueList':
|
||||||
|
return inputTypes.KEY_VALUE_LIST;
|
||||||
case 'password':
|
case 'password':
|
||||||
return inputTypes.PASSWORD;
|
return inputTypes.PASSWORD;
|
||||||
case 'number':
|
case 'number':
|
||||||
@@ -137,6 +139,8 @@ ProviderFieldFormGroup.propTypes = {
|
|||||||
type: PropTypes.string.isRequired,
|
type: PropTypes.string.isRequired,
|
||||||
advanced: PropTypes.bool.isRequired,
|
advanced: PropTypes.bool.isRequired,
|
||||||
hidden: PropTypes.string,
|
hidden: PropTypes.string,
|
||||||
|
isDisabled: PropTypes.bool,
|
||||||
|
provider: PropTypes.string,
|
||||||
pending: PropTypes.bool.isRequired,
|
pending: PropTypes.bool.isRequired,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
errors: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
warnings: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.image {
|
.image {
|
||||||
align-content: center;
|
align-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
|
|||||||
+1
@@ -2,6 +2,7 @@
|
|||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'image': string;
|
'image': string;
|
||||||
|
'wrapper': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ function ImdbRating(props: ImdbRatingProps) {
|
|||||||
return (
|
return (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
anchor={
|
anchor={
|
||||||
<span>
|
<span className={styles.wrapper}>
|
||||||
{!hideIcon && (
|
{!hideIcon && (
|
||||||
<img
|
<img
|
||||||
className={styles.image}
|
className={styles.image}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user