mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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 |
@@ -15,7 +15,7 @@ Note that only one type of a given movie is supported. If you want both a 4k ver
|
|||||||
|
|
||||||
* 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-2024
|
||||||
|
|||||||
+2
-2
@@ -9,7 +9,7 @@ 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.13.1'
|
majorVersion: '5.17.1'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
@@ -1226,7 +1226,7 @@ stages:
|
|||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@2
|
||||||
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'
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -186,7 +187,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.39'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -154,9 +154,14 @@ function HistoryRow(props: HistoryRowProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
if (name === 'downloadClient') {
|
||||||
|
const downloadClientName =
|
||||||
|
'downloadClientName' in data ? data.downloadClientName : null;
|
||||||
|
const downloadClient =
|
||||||
|
'downloadClient' in data ? data.downloadClient : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name} className={styles.downloadClient}>
|
<TableRowCell key={name} className={styles.downloadClient}>
|
||||||
{'downloadClient' in data ? data.downloadClient : ''}
|
{downloadClientName ?? downloadClient ?? ''}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 +1,16 @@
|
|||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
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,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;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import MovieCredit from 'typings/MovieCredit';
|
import MovieCredit from 'typings/MovieCredit';
|
||||||
|
|
||||||
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
|
type MovieCreditAppState = AppSectionState<MovieCredit>;
|
||||||
|
|
||||||
export default MovieCreditAppState;
|
export default MovieCreditAppState;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import NamingConfig from 'typings/Settings/NamingConfig';
|
|||||||
import NamingExample from 'typings/Settings/NamingExample';
|
import NamingExample from 'typings/Settings/NamingExample';
|
||||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
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>,
|
||||||
@@ -36,8 +37,7 @@ export interface NamingAppState
|
|||||||
extends AppSectionItemState<NamingConfig>,
|
extends AppSectionItemState<NamingConfig>,
|
||||||
AppSectionSaveState {}
|
AppSectionSaveState {}
|
||||||
|
|
||||||
export interface NamingExamplesAppState
|
export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
|
||||||
extends AppSectionItemState<NamingExample> {}
|
|
||||||
|
|
||||||
export interface ImportListAppState
|
export interface ImportListAppState
|
||||||
extends AppSectionState<ImportList>,
|
extends AppSectionState<ImportList>,
|
||||||
@@ -97,6 +97,7 @@ interface SettingsAppState {
|
|||||||
indexerFlags: IndexerFlagSettingsAppState;
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
|
metadata: MetadataAppState;
|
||||||
naming: NamingAppState;
|
naming: NamingAppState;
|
||||||
namingExamples: NamingExamplesAppState;
|
namingExamples: NamingExamplesAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,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;
|
||||||
@@ -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,7 +1,7 @@
|
|||||||
import React, { ComponentPropsWithoutRef } from 'react';
|
import React, { ComponentPropsWithoutRef } from 'react';
|
||||||
import styles from './TableRowCell.css';
|
import styles from './TableRowCell.css';
|
||||||
|
|
||||||
export interface TableRowCellProps extends ComponentPropsWithoutRef<'td'> {}
|
export type TableRowCellProps = ComponentPropsWithoutRef<'td'>;
|
||||||
|
|
||||||
export default function TableRowCell({
|
export default function TableRowCell({
|
||||||
className = styles.cell,
|
className = styles.cell,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "Radarr",
|
"name": "__INSTANCE_NAME__",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
|
"src": "__URL_BASE__/Content/Images/Icons/android-chrome-192x192.png",
|
||||||
|
|||||||
@@ -85,10 +85,16 @@ $hoverScale: 1.05;
|
|||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overviewContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 1 1000px;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
composes: link;
|
composes: link;
|
||||||
|
|
||||||
flex: 0 1 1000px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface CssExports {
|
|||||||
'link': string;
|
'link': string;
|
||||||
'lists': string;
|
'lists': string;
|
||||||
'overview': string;
|
'overview': string;
|
||||||
|
'overviewContainer': string;
|
||||||
'poster': string;
|
'poster': string;
|
||||||
'posterContainer': string;
|
'posterContainer': string;
|
||||||
'title': string;
|
'title': string;
|
||||||
|
|||||||
@@ -133,14 +133,20 @@ class DiscoverMovieOverview extends Component {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<MoviePoster
|
<Link
|
||||||
className={styles.poster}
|
className={styles.link}
|
||||||
style={elementStyle}
|
style={elementStyle}
|
||||||
images={images}
|
{...linkProps}
|
||||||
size={250}
|
>
|
||||||
lazy={false}
|
<MoviePoster
|
||||||
overflow={true}
|
className={styles.poster}
|
||||||
/>
|
style={elementStyle}
|
||||||
|
images={images}
|
||||||
|
size={250}
|
||||||
|
lazy={false}
|
||||||
|
overflow={true}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -242,11 +248,13 @@ class DiscoverMovieOverview extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div className={styles.overview}>
|
<div className={styles.overviewContainer}>
|
||||||
<TextTruncate
|
<Link className={styles.overview} {...linkProps}>
|
||||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
<TextTruncate
|
||||||
text={overview}
|
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||||
/>
|
text={overview}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DiscoverMovieOverviewInfo
|
<DiscoverMovieOverviewInfo
|
||||||
@@ -255,7 +263,6 @@ class DiscoverMovieOverview extends Component {
|
|||||||
{...overviewOptions}
|
{...overviewOptions}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function isVisible(row, props) {
|
|||||||
valueProp
|
valueProp
|
||||||
} = row;
|
} = row;
|
||||||
|
|
||||||
return _.has(props, valueProp) && (_.get(props, showProp) || props.sortKey === name);
|
return _.has(props, valueProp) && _.get(props, valueProp) !== null && (props[showProp] || props.sortKey === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInfoRowProps(row, props) {
|
function getInfoRowProps(row, props) {
|
||||||
|
|||||||
@@ -50,15 +50,13 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sortKey === 'inCinemas' && inCinemas) {
|
if (sortKey === 'inCinemas' && inCinemas) {
|
||||||
const inCinemasDate = getRelativeDate(
|
const inCinemasDate = getRelativeDate({
|
||||||
inCinemas,
|
date: inCinemas,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
{
|
timeFormat,
|
||||||
timeFormat,
|
timeForToday: false
|
||||||
timeForToday: false
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('InCinemas')}>
|
<div className={styles.info} title={translate('InCinemas')}>
|
||||||
@@ -68,15 +66,13 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sortKey === 'digitalRelease' && digitalRelease) {
|
if (sortKey === 'digitalRelease' && digitalRelease) {
|
||||||
const digitalReleaseDate = getRelativeDate(
|
const digitalReleaseDate = getRelativeDate({
|
||||||
digitalRelease,
|
date: digitalRelease,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
{
|
timeFormat,
|
||||||
timeFormat,
|
timeForToday: false
|
||||||
timeForToday: false
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('DigitalRelease')}>
|
<div className={styles.info} title={translate('DigitalRelease')}>
|
||||||
@@ -86,15 +82,13 @@ function DiscoverMoviePosterInfo(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sortKey === 'physicalRelease' && physicalRelease) {
|
if (sortKey === 'physicalRelease' && physicalRelease) {
|
||||||
const physicalReleaseDate = getRelativeDate(
|
const physicalReleaseDate = getRelativeDate({
|
||||||
physicalRelease,
|
date: physicalRelease,
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
showRelativeDates,
|
showRelativeDates,
|
||||||
{
|
timeFormat,
|
||||||
timeFormat,
|
timeForToday: false
|
||||||
timeForToday: false
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.info} title={translate('PhysicalRelease')}>
|
<div className={styles.info} title={translate('PhysicalRelease')}>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const LANGUAGE = 'language';
|
|||||||
export const PROTOCOL = 'protocol';
|
export const PROTOCOL = 'protocol';
|
||||||
export const QUALITY = 'quality';
|
export const QUALITY = 'quality';
|
||||||
export const QUALITY_PROFILE = 'qualityProfile';
|
export const QUALITY_PROFILE = 'qualityProfile';
|
||||||
|
export const QUEUE_STATUS = 'queueStatus';
|
||||||
export const MOVIE = 'movie';
|
export const MOVIE = 'movie';
|
||||||
export const RELEASE_STATUS = 'releaseStatus';
|
export const RELEASE_STATUS = 'releaseStatus';
|
||||||
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
|
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
faFileVideo as farFileVideo,
|
faFileVideo as farFileVideo,
|
||||||
faFolder as farFolder,
|
faFolder as farFolder,
|
||||||
faHdd as farHdd,
|
faHdd as farHdd,
|
||||||
|
faHeart as farHeart,
|
||||||
faKeyboard as farKeyboard,
|
faKeyboard as farKeyboard,
|
||||||
faObjectGroup as farObjectGroup,
|
faObjectGroup as farObjectGroup,
|
||||||
faObjectUngroup as farObjectUngroup,
|
faObjectUngroup as farObjectUngroup,
|
||||||
@@ -174,6 +175,7 @@ export const GENRE = fasTheaterMasks;
|
|||||||
export const GROUP = farObjectGroup;
|
export const GROUP = farObjectGroup;
|
||||||
export const HEALTH = fasMedkit;
|
export const HEALTH = fasMedkit;
|
||||||
export const HEART = fasHeart;
|
export const HEART = fasHeart;
|
||||||
|
export const HEART_OUTLINE = farHeart;
|
||||||
export const HISTORY = fasHistory;
|
export const HISTORY = fasHistory;
|
||||||
export const HOUSEKEEPING = fasHome;
|
export const HOUSEKEEPING = fasHome;
|
||||||
export const IGNORE = fasTimesCircle;
|
export const IGNORE = fasTimesCircle;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.actions {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 70px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'actions': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import React, { SyntheticEvent, useCallback } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { removeFavoriteFolder } from 'Store/Actions/interactiveImportActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './FavoriteFolderRow.css';
|
||||||
|
|
||||||
|
interface FavoriteFolderRowProps {
|
||||||
|
folder: string;
|
||||||
|
onPress: (folder: string) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function FavoriteFolderRow({ folder, onPress }: FavoriteFolderRowProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
onPress(folder);
|
||||||
|
}, [folder, onPress]);
|
||||||
|
|
||||||
|
const handleRemoveFavoritePress = useCallback(
|
||||||
|
(e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
dispatch(removeFavoriteFolder({ folder }));
|
||||||
|
},
|
||||||
|
[folder, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowButton onPress={handlePress}>
|
||||||
|
<TableRowCell>{folder}</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
title={translate('FavoriteFolderRemove')}
|
||||||
|
kind="danger"
|
||||||
|
name={icons.HEART}
|
||||||
|
onPress={handleRemoveFavoritePress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
</TableRowButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FavoriteFolderRow;
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
.recentFoldersContainer {
|
.foldersContainer {
|
||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.foldersTitle {
|
||||||
|
border-bottom: 1px solid var(--borderColor);
|
||||||
|
font-size: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
.buttonsContainer {
|
.buttonsContainer {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -5,7 +5,8 @@ interface CssExports {
|
|||||||
'buttonContainer': string;
|
'buttonContainer': string;
|
||||||
'buttonIcon': string;
|
'buttonIcon': string;
|
||||||
'buttonsContainer': string;
|
'buttonsContainer': string;
|
||||||
'recentFoldersContainer': string;
|
'foldersContainer': string;
|
||||||
|
'foldersTitle': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
+53
-18
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
@@ -14,14 +14,23 @@ import Table from 'Components/Table/Table';
|
|||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import {
|
import { addRecentFolder } from 'Store/Actions/interactiveImportActions';
|
||||||
addRecentFolder,
|
|
||||||
removeRecentFolder,
|
|
||||||
} from 'Store/Actions/interactiveImportActions';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import FavoriteFolderRow from './FavoriteFolderRow';
|
||||||
import RecentFolderRow from './RecentFolderRow';
|
import RecentFolderRow from './RecentFolderRow';
|
||||||
import styles from './InteractiveImportSelectFolderModalContent.css';
|
import styles from './InteractiveImportSelectFolderModalContent.css';
|
||||||
|
|
||||||
|
const favoriteFoldersColumns = [
|
||||||
|
{
|
||||||
|
name: 'folder',
|
||||||
|
label: () => translate('Folder'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const recentFoldersColumns = [
|
const recentFoldersColumns = [
|
||||||
{
|
{
|
||||||
name: 'folder',
|
name: 'folder',
|
||||||
@@ -49,15 +58,22 @@ function InteractiveImportSelectFolderModalContent(
|
|||||||
const { modalTitle, onFolderSelect, onModalClose } = props;
|
const { modalTitle, onFolderSelect, onModalClose } = props;
|
||||||
const [folder, setFolder] = useState('');
|
const [folder, setFolder] = useState('');
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const recentFolders = useSelector(
|
const { favoriteFolders, recentFolders } = useSelector(
|
||||||
createSelector(
|
createSelector(
|
||||||
(state: AppState) => state.interactiveImport.recentFolders,
|
(state: AppState) => state.interactiveImport,
|
||||||
(recentFolders) => {
|
(interactiveImport) => {
|
||||||
return recentFolders;
|
return {
|
||||||
|
favoriteFolders: interactiveImport.favoriteFolders,
|
||||||
|
recentFolders: interactiveImport.recentFolders,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const favoriteFolderMap = useMemo(() => {
|
||||||
|
return new Map(favoriteFolders.map((f) => [f.folder, f]));
|
||||||
|
}, [favoriteFolders]);
|
||||||
|
|
||||||
const onPathChange = useCallback(
|
const onPathChange = useCallback(
|
||||||
({ value }: { value: string }) => {
|
({ value }: { value: string }) => {
|
||||||
setFolder(value);
|
setFolder(value);
|
||||||
@@ -90,13 +106,6 @@ function InteractiveImportSelectFolderModalContent(
|
|||||||
onFolderSelect(folder);
|
onFolderSelect(folder);
|
||||||
}, [folder, onFolderSelect, dispatch]);
|
}, [folder, onFolderSelect, dispatch]);
|
||||||
|
|
||||||
const onRemoveRecentFolderPress = useCallback(
|
|
||||||
(folderToRemove: string) => {
|
|
||||||
dispatch(removeRecentFolder({ folder: folderToRemove }));
|
|
||||||
},
|
|
||||||
[dispatch]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
@@ -110,8 +119,34 @@ function InteractiveImportSelectFolderModalContent(
|
|||||||
onChange={onPathChange}
|
onChange={onPathChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{favoriteFolders.length ? (
|
||||||
|
<div className={styles.foldersContainer}>
|
||||||
|
<div className={styles.foldersTitle}>
|
||||||
|
{translate('FavoriteFolders')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table columns={favoriteFoldersColumns}>
|
||||||
|
<TableBody>
|
||||||
|
{favoriteFolders.map((favoriteFolder) => {
|
||||||
|
return (
|
||||||
|
<FavoriteFolderRow
|
||||||
|
key={favoriteFolder.folder}
|
||||||
|
folder={favoriteFolder.folder}
|
||||||
|
onPress={onRecentPathPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{recentFolders.length ? (
|
{recentFolders.length ? (
|
||||||
<div className={styles.recentFoldersContainer}>
|
<div className={styles.foldersContainer}>
|
||||||
|
<div className={styles.foldersTitle}>
|
||||||
|
{translate('RecentFolders')}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Table columns={recentFoldersColumns}>
|
<Table columns={recentFoldersColumns}>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{recentFolders
|
{recentFolders
|
||||||
@@ -123,8 +158,8 @@ function InteractiveImportSelectFolderModalContent(
|
|||||||
key={recentFolder.folder}
|
key={recentFolder.folder}
|
||||||
folder={recentFolder.folder}
|
folder={recentFolder.folder}
|
||||||
lastUsed={recentFolder.lastUsed}
|
lastUsed={recentFolder.lastUsed}
|
||||||
|
isFavorite={favoriteFolderMap.has(recentFolder.folder)}
|
||||||
onPress={onRecentPathPress}
|
onPress={onRecentPathPress}
|
||||||
onRemoveRecentFolderPress={onRemoveRecentFolderPress}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
interface RecentFolder {
|
|
||||||
folder: string;
|
|
||||||
lastUsed: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RecentFolder;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
.actions {
|
.actions {
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
width: 40px;
|
width: 70px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +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 TableRowButton from 'Components/Table/TableRowButton';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './RecentFolderRow.css';
|
|
||||||
|
|
||||||
class RecentFolderRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.props.onPress(this.props.folder);
|
|
||||||
};
|
|
||||||
|
|
||||||
onRemovePress = (event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
const {
|
|
||||||
folder,
|
|
||||||
onRemoveRecentFolderPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onRemoveRecentFolderPress(folder);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
folder,
|
|
||||||
lastUsed
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowButton onPress={this.onPress}>
|
|
||||||
<TableRowCell>{folder}</TableRowCell>
|
|
||||||
|
|
||||||
<RelativeDateCell date={lastUsed} />
|
|
||||||
|
|
||||||
<TableRowCell className={styles.actions}>
|
|
||||||
<IconButton
|
|
||||||
title={translate('Remove')}
|
|
||||||
name={icons.REMOVE}
|
|
||||||
onPress={this.onRemovePress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
</TableRowButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RecentFolderRow.propTypes = {
|
|
||||||
folder: PropTypes.string.isRequired,
|
|
||||||
lastUsed: PropTypes.string.isRequired,
|
|
||||||
onPress: PropTypes.func.isRequired,
|
|
||||||
onRemoveRecentFolderPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RecentFolderRow;
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { SyntheticEvent, useCallback } 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 TableRowButton from 'Components/Table/TableRowButton';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
addFavoriteFolder,
|
||||||
|
removeFavoriteFolder,
|
||||||
|
removeRecentFolder,
|
||||||
|
} from 'Store/Actions/interactiveImportActions';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './RecentFolderRow.css';
|
||||||
|
|
||||||
|
interface RecentFolderRowProps {
|
||||||
|
folder: string;
|
||||||
|
lastUsed: string;
|
||||||
|
isFavorite: boolean;
|
||||||
|
onPress: (folder: string) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RecentFolderRow({
|
||||||
|
folder,
|
||||||
|
lastUsed,
|
||||||
|
isFavorite,
|
||||||
|
onPress,
|
||||||
|
}: RecentFolderRowProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const handlePress = useCallback(() => {
|
||||||
|
onPress(folder);
|
||||||
|
}, [folder, onPress]);
|
||||||
|
|
||||||
|
const handleFavoritePress = useCallback(
|
||||||
|
(e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isFavorite) {
|
||||||
|
dispatch(removeFavoriteFolder({ folder }));
|
||||||
|
} else {
|
||||||
|
dispatch(addFavoriteFolder({ folder }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[folder, isFavorite, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRemovePress = useCallback(
|
||||||
|
(e: SyntheticEvent) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
dispatch(removeRecentFolder({ folder }));
|
||||||
|
},
|
||||||
|
[folder, dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowButton onPress={handlePress}>
|
||||||
|
<TableRowCell>{folder}</TableRowCell>
|
||||||
|
|
||||||
|
<RelativeDateCell date={lastUsed} />
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
title={
|
||||||
|
isFavorite
|
||||||
|
? translate('FavoriteFolderRemove')
|
||||||
|
: translate('FavoriteFolderAdd')
|
||||||
|
}
|
||||||
|
kind={isFavorite ? 'danger' : 'default'}
|
||||||
|
name={isFavorite ? icons.HEART : icons.HEART_OUTLINE}
|
||||||
|
onPress={handleFavoritePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
title={translate('Remove')}
|
||||||
|
name={icons.REMOVE}
|
||||||
|
onPress={handleRemovePress}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
</TableRowButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RecentFolderRow;
|
||||||
@@ -28,6 +28,7 @@ import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
|||||||
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
|
||||||
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
|
||||||
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
|
||||||
|
import MovieGenres from 'Movie/MovieGenres';
|
||||||
import MoviePoster from 'Movie/MoviePoster';
|
import MoviePoster from 'Movie/MoviePoster';
|
||||||
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
|
||||||
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
|
||||||
@@ -651,9 +652,7 @@ class MovieDetails extends Component {
|
|||||||
name={translate('Genres')}
|
name={translate('Genres')}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
<span className={styles.genres}>
|
<MovieGenres className={styles.genres} genres={genres} />
|
||||||
{genres.join(', ')}
|
|
||||||
</span>
|
|
||||||
</InfoLabel> :
|
</InfoLabel> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
|
|
||||||
|
interface MovieGenresProps {
|
||||||
|
className?: string;
|
||||||
|
genres: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieGenres({ className, genres }: MovieGenresProps) {
|
||||||
|
const firstGenres = genres.slice(0, 3);
|
||||||
|
const otherGenres = genres.slice(3);
|
||||||
|
|
||||||
|
if (otherGenres.length) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
anchor={<span className={className}>{firstGenres.join(', ')}</span>}
|
||||||
|
tooltip={
|
||||||
|
<div>
|
||||||
|
{otherGenres.map((tag) => {
|
||||||
|
return (
|
||||||
|
<Label key={tag} kind={kinds.INFO} size={sizes.LARGE}>
|
||||||
|
{tag}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className={className}>{firstGenres.join(', ')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieGenres;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import MediaInfoProps from 'typings/MediaInfo';
|
||||||
|
import getEntries from 'Utilities/Object/getEntries';
|
||||||
|
|
||||||
|
function MediaInfo(props: MediaInfoProps) {
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
{getEntries(props).map(([key, value]) => {
|
||||||
|
const title = key
|
||||||
|
.replace(/([A-Z])/g, ' $1')
|
||||||
|
.replace(/^./, (str) => str.toUpperCase());
|
||||||
|
|
||||||
|
if (!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionListItem key={key} title={title} data={props[key]} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaInfo;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
|
|
||||||
function MediaInfoPopover(props) {
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
{
|
|
||||||
Object.keys(props).map((key) => {
|
|
||||||
const title = key
|
|
||||||
.replace(/([A-Z])/g, ' $1')
|
|
||||||
.replace(/^./, (str) => str.toUpperCase());
|
|
||||||
|
|
||||||
const value = props[key];
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionListItem
|
|
||||||
key={key}
|
|
||||||
title={title}
|
|
||||||
data={props[key]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MediaInfoPopover;
|
|
||||||
@@ -14,7 +14,7 @@ import MovieFormats from 'Movie/MovieFormats';
|
|||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
import FileEditModal from 'MovieFile/Edit/FileEditModal';
|
import FileEditModal from 'MovieFile/Edit/FileEditModal';
|
||||||
import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
|
import MediaInfo from 'MovieFile/MediaInfo';
|
||||||
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
@@ -224,7 +224,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.audio}
|
className={styles.audio}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.AUDIO}
|
type={mediaInfoTypes.AUDIO}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -238,7 +238,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.audioLanguages}
|
className={styles.audioLanguages}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
type={mediaInfoTypes.AUDIO_LANGUAGES}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -252,7 +252,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.subtitles}
|
className={styles.subtitles}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.SUBTITLES}
|
type={mediaInfoTypes.SUBTITLES}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -266,7 +266,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.video}
|
className={styles.video}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.VIDEO}
|
type={mediaInfoTypes.VIDEO}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
@@ -280,7 +280,7 @@ class MovieFileEditorRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.videoDynamicRangeType}
|
className={styles.videoDynamicRangeType}
|
||||||
>
|
>
|
||||||
<MediaInfoConnector
|
<MediaInfo
|
||||||
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
type={mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE}
|
||||||
movieFileId={id}
|
movieFileId={id}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
|||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { sizes } from 'Helpers/Props';
|
import { sizes } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MediaInfoPopover from './Editor/MediaInfoPopover';
|
import MediaInfo from './Editor/MediaInfo';
|
||||||
|
|
||||||
function FileDetailsModal(props) {
|
function FileDetailsModal(props) {
|
||||||
const {
|
const {
|
||||||
@@ -31,7 +31,7 @@ function FileDetailsModal(props) {
|
|||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<MediaInfoPopover {...mediaInfo} />
|
<MediaInfo {...mediaInfo} />
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import getLanguageName from 'Utilities/String/getLanguageName';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import * as mediaInfoTypes from './mediaInfoTypes';
|
|
||||||
|
|
||||||
function formatLanguages(languages) {
|
|
||||||
if (!languages) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const splitLanguages = _.uniq(languages.split('/')).map((l) => {
|
|
||||||
const simpleLanguage = l.split('_')[0];
|
|
||||||
|
|
||||||
if (simpleLanguage === 'und') {
|
|
||||||
return translate('Unknown');
|
|
||||||
}
|
|
||||||
|
|
||||||
return getLanguageName(simpleLanguage);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (splitLanguages.length > 3) {
|
|
||||||
return (
|
|
||||||
<span title={splitLanguages.join(', ')}>
|
|
||||||
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2} more
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{splitLanguages.join(', ')}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MediaInfo(props) {
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
audioChannels,
|
|
||||||
audioCodec,
|
|
||||||
audioLanguages,
|
|
||||||
subtitles,
|
|
||||||
videoCodec,
|
|
||||||
videoDynamicRangeType
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.AUDIO) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{
|
|
||||||
audioCodec ? audioCodec : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
audioCodec && audioChannels ? ' - ' : ''
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
audioChannels ? audioChannels.toFixed(1) : ''
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.AUDIO_LANGUAGES) {
|
|
||||||
return formatLanguages(audioLanguages);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.SUBTITLES) {
|
|
||||||
return formatLanguages(subtitles);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.VIDEO) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{videoCodec}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === mediaInfoTypes.VIDEO_DYNAMIC_RANGE_TYPE) {
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
{videoDynamicRangeType}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
MediaInfo.propTypes = {
|
|
||||||
type: PropTypes.string.isRequired,
|
|
||||||
audioChannels: PropTypes.number,
|
|
||||||
audioCodec: PropTypes.string,
|
|
||||||
audioLanguages: PropTypes.string,
|
|
||||||
subtitles: PropTypes.string,
|
|
||||||
videoCodec: PropTypes.string,
|
|
||||||
videoDynamicRangeType: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MediaInfo;
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import getLanguageName from 'Utilities/String/getLanguageName';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import useMovieFile from './useMovieFile';
|
||||||
|
|
||||||
|
function formatLanguages(languages: string | undefined) {
|
||||||
|
if (!languages) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const splitLanguages = [...new Set(languages.split('/'))].map((l) => {
|
||||||
|
const simpleLanguage = l.split('_')[0];
|
||||||
|
|
||||||
|
if (simpleLanguage === 'und') {
|
||||||
|
return translate('Unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
return getLanguageName(simpleLanguage);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (splitLanguages.length > 3) {
|
||||||
|
return (
|
||||||
|
<span title={splitLanguages.join(', ')}>
|
||||||
|
{splitLanguages.slice(0, 2).join(', ')}, {splitLanguages.length - 2}{' '}
|
||||||
|
more
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{splitLanguages.join(', ')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MediaInfoType =
|
||||||
|
| 'audio'
|
||||||
|
| 'audioLanguages'
|
||||||
|
| 'subtitles'
|
||||||
|
| 'video'
|
||||||
|
| 'videoDynamicRangeType';
|
||||||
|
|
||||||
|
interface MediaInfoProps {
|
||||||
|
movieFileId?: number;
|
||||||
|
type: MediaInfoType;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MediaInfo({ movieFileId, type }: MediaInfoProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
|
||||||
|
if (!movieFile?.mediaInfo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
audioChannels,
|
||||||
|
audioCodec,
|
||||||
|
audioLanguages,
|
||||||
|
subtitles,
|
||||||
|
videoCodec,
|
||||||
|
videoDynamicRangeType,
|
||||||
|
} = movieFile.mediaInfo;
|
||||||
|
|
||||||
|
if (type === 'audio') {
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{audioCodec ? audioCodec : ''}
|
||||||
|
|
||||||
|
{audioCodec && audioChannels ? ' - ' : ''}
|
||||||
|
|
||||||
|
{audioChannels ? audioChannels.toFixed(1) : ''}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'audioLanguages') {
|
||||||
|
return formatLanguages(audioLanguages);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'subtitles') {
|
||||||
|
return formatLanguages(subtitles);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'video') {
|
||||||
|
return <span>{videoCodec}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'videoDynamicRangeType') {
|
||||||
|
return <span>{videoDynamicRangeType}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MediaInfo;
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
|
||||||
import MediaInfo from './MediaInfo';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieFileSelector(),
|
|
||||||
(movieFile) => {
|
|
||||||
if (movieFile) {
|
|
||||||
return {
|
|
||||||
...movieFile.mediaInfo
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MediaInfo);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
|
||||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieFileSelector(),
|
|
||||||
(movieFile) => {
|
|
||||||
return {
|
|
||||||
languages: movieFile ? movieFile.languages : undefined
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(MovieLanguages);
|
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
|
import useMovieFile from './useMovieFile';
|
||||||
|
|
||||||
|
interface MovieFileLanguagesProps {
|
||||||
|
movieFileId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieFileLanguages({ movieFileId }: MovieFileLanguagesProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
|
||||||
|
return <MovieLanguages languages={movieFile?.languages ?? []} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieFileLanguages;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
|
||||||
|
function createMovieFileSelector(movieFileId?: number) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.movieFiles.items,
|
||||||
|
(movieFiles) => {
|
||||||
|
return movieFiles.find(({ id }) => id === movieFileId);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMovieFile(movieFileId: number | undefined) {
|
||||||
|
return useSelector(createMovieFileSelector(movieFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useMovieFile;
|
||||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
import EditCustomFormatModal from './EditCustomFormatModal';
|
import EditCustomFormatModal from './EditCustomFormatModal';
|
||||||
|
import EditCustomFormatModalContentConnector from './EditCustomFormatModalContentConnector';
|
||||||
|
|
||||||
function mapStateToProps() {
|
function mapStateToProps() {
|
||||||
return {};
|
return {};
|
||||||
@@ -36,6 +37,7 @@ class EditCustomFormatModalConnector extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
EditCustomFormatModalConnector.propTypes = {
|
EditCustomFormatModalConnector.propTypes = {
|
||||||
|
...EditCustomFormatModalContentConnector.propTypes,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
clearPendingChanges: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|||||||
+5
@@ -47,6 +47,11 @@ const COLUMNS = [
|
|||||||
isSortable: true,
|
isSortable: true,
|
||||||
isVisible: true,
|
isVisible: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: '',
|
||||||
|
isVisible: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
interface ManageCustomFormatsModalContentProps {
|
interface ManageCustomFormatsModalContentProps {
|
||||||
|
|||||||
+6
@@ -4,3 +4,9 @@
|
|||||||
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'actions': string;
|
||||||
'includeCustomFormatWhenRenaming': string;
|
'includeCustomFormatWhenRenaming': string;
|
||||||
'name': string;
|
'name': string;
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-3
@@ -1,10 +1,18 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import { deleteCustomFormat } from 'Store/Actions/settingsActions';
|
||||||
import { SelectStateInputProps } from 'typings/props';
|
import { SelectStateInputProps } from 'typings/props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditCustomFormatModalConnector from '../EditCustomFormatModalConnector';
|
||||||
import styles from './ManageCustomFormatsModalRow.css';
|
import styles from './ManageCustomFormatsModalRow.css';
|
||||||
|
|
||||||
interface ManageCustomFormatsModalRowProps {
|
interface ManageCustomFormatsModalRowProps {
|
||||||
@@ -16,6 +24,15 @@ interface ManageCustomFormatsModalRowProps {
|
|||||||
onSelectedChange(result: SelectStateInputProps): void;
|
onSelectedChange(result: SelectStateInputProps): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isDeletingSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.settings.customFormats.isDeleting,
|
||||||
|
(isDeleting) => {
|
||||||
|
return isDeleting;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
@@ -25,7 +42,16 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
|||||||
onSelectedChange,
|
onSelectedChange,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const onSelectedChangeWrapper = useCallback(
|
const dispatch = useDispatch();
|
||||||
|
const isDeleting = useSelector(isDeletingSelector());
|
||||||
|
|
||||||
|
const [isEditCustomFormatModalOpen, setIsEditCustomFormatModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const [isDeleteCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const handlelectedChange = useCallback(
|
||||||
(result: SelectStateInputProps) => {
|
(result: SelectStateInputProps) => {
|
||||||
onSelectedChange({
|
onSelectedChange({
|
||||||
...result,
|
...result,
|
||||||
@@ -34,12 +60,33 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
|||||||
[onSelectedChange]
|
[onSelectedChange]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleEditCustomFormatModalOpen = useCallback(() => {
|
||||||
|
setIsEditCustomFormatModalOpen(true);
|
||||||
|
}, [setIsEditCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleEditCustomFormatModalClose = useCallback(() => {
|
||||||
|
setIsEditCustomFormatModalOpen(false);
|
||||||
|
}, [setIsEditCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteCustomFormatPress = useCallback(() => {
|
||||||
|
setIsEditCustomFormatModalOpen(false);
|
||||||
|
setIsDeleteCustomFormatModalOpen(true);
|
||||||
|
}, [setIsEditCustomFormatModalOpen, setIsDeleteCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleDeleteCustomFormatModalClose = useCallback(() => {
|
||||||
|
setIsDeleteCustomFormatModalOpen(false);
|
||||||
|
}, [setIsDeleteCustomFormatModalOpen]);
|
||||||
|
|
||||||
|
const handleConfirmDeleteCustomFormat = useCallback(() => {
|
||||||
|
dispatch(deleteCustomFormat({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableSelectCell
|
<TableSelectCell
|
||||||
id={id}
|
id={id}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
onSelectedChange={onSelectedChangeWrapper}
|
onSelectedChange={handlelectedChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
<TableRowCell className={styles.name}>{name}</TableRowCell>
|
||||||
@@ -47,6 +94,31 @@ function ManageCustomFormatsModalRow(props: ManageCustomFormatsModalRowProps) {
|
|||||||
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
|
<TableRowCell className={styles.includeCustomFormatWhenRenaming}>
|
||||||
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
|
{includeCustomFormatWhenRenaming ? translate('Yes') : translate('No')}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.actions}>
|
||||||
|
<IconButton
|
||||||
|
name={icons.EDIT}
|
||||||
|
onPress={handleEditCustomFormatModalOpen}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<EditCustomFormatModalConnector
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditCustomFormatModalOpen}
|
||||||
|
onModalClose={handleEditCustomFormatModalClose}
|
||||||
|
onDeleteCustomFormatPress={handleDeleteCustomFormatPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isDeleteCustomFormatModalOpen}
|
||||||
|
kind="danger"
|
||||||
|
title={translate('DeleteCustomFormat')}
|
||||||
|
message={translate('DeleteCustomFormatMessageText', { name })}
|
||||||
|
confirmLabel={translate('Delete')}
|
||||||
|
isSpinning={isDeleting}
|
||||||
|
onConfirm={handleConfirmDeleteCustomFormat}
|
||||||
|
onCancel={handleDeleteCustomFormatModalClose}
|
||||||
|
/>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ function ManageCustomFormatsToolbarButton() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('ManageCustomFormats')}
|
label={translate('ManageFormats')}
|
||||||
iconName={icons.MANAGE}
|
iconName={icons.MANAGE}
|
||||||
onPress={openManageModal}
|
onPress={openManageModal}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+2
-2
@@ -55,10 +55,10 @@ function EditSpecificationModalContent(props) {
|
|||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ function createImportListExclusionSelector(id?: number) {
|
|||||||
importListExclusions;
|
importListExclusions;
|
||||||
|
|
||||||
const mapping = id
|
const mapping = id
|
||||||
? items.find((i) => i.id === id)
|
? items.find((i) => i.id === id)!
|
||||||
: newImportListExclusion;
|
: newImportListExclusion;
|
||||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
const settings = selectSettings(mapping, pendingChanges, saveError);
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import { sizes } from 'Helpers/Props';
|
|
||||||
import EditMetadataModalContentConnector from './EditMetadataModalContentConnector';
|
|
||||||
|
|
||||||
function EditMetadataModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
size={sizes.MEDIUM}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<EditMetadataModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditMetadataModal;
|
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||||
|
import EditMetadataModalContent, {
|
||||||
|
EditMetadataModalContentProps,
|
||||||
|
} from './EditMetadataModalContent';
|
||||||
|
|
||||||
|
interface EditMetadataModalProps
|
||||||
|
extends Omit<EditMetadataModalContentProps, 'advancedSettings'> {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditMetadataModal({
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
}: EditMetadataModalProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const advancedSettings = useSelector(
|
||||||
|
(state: AppState) => state.settings.advancedSettings
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
dispatch(clearPendingChanges({ section: 'metadata' }));
|
||||||
|
onModalClose();
|
||||||
|
}, [dispatch, onModalClose]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
|
<EditMetadataModalContent
|
||||||
|
{...otherProps}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMetadataModal;
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import EditMetadataModal from './EditMetadataModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
const section = 'settings.metadata';
|
|
||||||
|
|
||||||
return {
|
|
||||||
dispatchClearPendingChanges() {
|
|
||||||
dispatch(clearPendingChanges({ section }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class EditMetadataModalConnector extends Component {
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.dispatchClearPendingChanges({ section: 'metadata' });
|
|
||||||
this.props.onModalClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditMetadataModal
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModalConnector.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
dispatchClearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(EditMetadataModalConnector);
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.message {
|
||||||
|
composes: alert from '~Components/Alert.css';
|
||||||
|
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'message': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function EditMetadataModalContent(props) {
|
|
||||||
const {
|
|
||||||
advancedSettings,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item,
|
|
||||||
onInputChange,
|
|
||||||
onFieldChange,
|
|
||||||
onModalClose,
|
|
||||||
onSavePress,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
enable,
|
|
||||||
fields
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('EditMetadata', { metadataType: name.value })}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form {...otherProps}>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>{translate('Enable')}</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="enable"
|
|
||||||
helpText={translate('EnableMetadataHelpText')}
|
|
||||||
{...enable}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{
|
|
||||||
fields.map((field) => {
|
|
||||||
return (
|
|
||||||
<ProviderFieldFormGroup
|
|
||||||
key={field.name}
|
|
||||||
advancedSettings={advancedSettings}
|
|
||||||
provider="metadata"
|
|
||||||
{...field}
|
|
||||||
isDisabled={!enable.value}
|
|
||||||
onChange={onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Cancel')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerErrorButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
error={saveError}
|
|
||||||
onPress={onSavePress}
|
|
||||||
>
|
|
||||||
{translate('Save')}
|
|
||||||
</SpinnerErrorButton>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModalContent.propTypes = {
|
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onFieldChange: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
onSavePress: PropTypes.func.isRequired,
|
|
||||||
onDeleteMetadataPress: PropTypes.func
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditMetadataModalContent;
|
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import Form from 'Components/Form/Form';
|
||||||
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
|
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import { inputTypes } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
saveMetadata,
|
||||||
|
setMetadataFieldValue,
|
||||||
|
setMetadataValue,
|
||||||
|
} from 'Store/Actions/settingsActions';
|
||||||
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
|
import { InputChanged } from 'typings/inputs';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './EditMetadataModalContent.css';
|
||||||
|
|
||||||
|
export interface EditMetadataModalContentProps {
|
||||||
|
id: number;
|
||||||
|
advancedSettings: boolean;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditMetadataModalContent({
|
||||||
|
id,
|
||||||
|
advancedSettings,
|
||||||
|
onModalClose,
|
||||||
|
}: EditMetadataModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { isSaving, saveError, pendingChanges, items } = useSelector(
|
||||||
|
(state: AppState) => state.settings.metadata
|
||||||
|
);
|
||||||
|
|
||||||
|
const { settings, ...otherSettings } = useMemo(() => {
|
||||||
|
const item = items.find((item) => item.id === id)!;
|
||||||
|
|
||||||
|
return selectSettings(item, pendingChanges, saveError);
|
||||||
|
}, [id, items, pendingChanges, saveError]);
|
||||||
|
|
||||||
|
const { name, enable, fields, message } = settings;
|
||||||
|
|
||||||
|
const handleInputChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error not typed
|
||||||
|
dispatch(setMetadataValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleFieldChange = useCallback(
|
||||||
|
({ name, value }: InputChanged) => {
|
||||||
|
// @ts-expect-error not typed
|
||||||
|
dispatch(setMetadataFieldValue({ name, value }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSavePress = useCallback(() => {
|
||||||
|
dispatch(saveMetadata({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('EditMetadata', { metadataType: name.value })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Form {...otherSettings}>
|
||||||
|
{message ? (
|
||||||
|
<Alert className={styles.message} kind={message.value.type}>
|
||||||
|
{message.value.message}
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('Enable')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="enable"
|
||||||
|
helpText={translate('EnableMetadataHelpText')}
|
||||||
|
{...enable}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{fields.map((field) => {
|
||||||
|
return (
|
||||||
|
<ProviderFieldFormGroup
|
||||||
|
key={field.name}
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
provider="metadata"
|
||||||
|
{...field}
|
||||||
|
isDisabled={!enable.value}
|
||||||
|
onChange={handleFieldChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Form>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||||
|
|
||||||
|
<SpinnerErrorButton
|
||||||
|
isSpinning={isSaving}
|
||||||
|
error={saveError}
|
||||||
|
onPress={handleSavePress}
|
||||||
|
>
|
||||||
|
{translate('Save')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EditMetadataModalContent;
|
||||||
@@ -1,95 +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 { saveMetadata, setMetadataFieldValue, setMetadataValue } from 'Store/Actions/settingsActions';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import EditMetadataModalContent from './EditMetadataModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.settings.advancedSettings,
|
|
||||||
(state, { id }) => id,
|
|
||||||
(state) => state.settings.metadata,
|
|
||||||
(advancedSettings, id, metadata) => {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
pendingChanges,
|
|
||||||
items
|
|
||||||
} = metadata;
|
|
||||||
|
|
||||||
const settings = selectSettings(_.find(items, { id }), pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
advancedSettings,
|
|
||||||
id,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item: settings.settings,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setMetadataValue,
|
|
||||||
setMetadataFieldValue,
|
|
||||||
saveMetadata
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditMetadataModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
|
||||||
this.props.onModalClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.setMetadataValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFieldChange = ({ name, value }) => {
|
|
||||||
this.props.setMetadataFieldValue({ name, value });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
this.props.saveMetadata({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditMetadataModalContent
|
|
||||||
{...this.props}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onFieldChange={this.onFieldChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditMetadataModalContentConnector.propTypes = {
|
|
||||||
id: PropTypes.number,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
setMetadataValue: PropTypes.func.isRequired,
|
|
||||||
setMetadataFieldValue: PropTypes.func.isRequired,
|
|
||||||
saveMetadata: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditMetadataModalContentConnector);
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Card from 'Components/Card';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import EditMetadataModalConnector from './EditMetadataModalConnector';
|
|
||||||
import styles from './Metadata.css';
|
|
||||||
|
|
||||||
class Metadata extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isEditMetadataModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEditMetadataPress = () => {
|
|
||||||
this.setState({ isEditMetadataModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onEditMetadataModalClose = () => {
|
|
||||||
this.setState({ isEditMetadataModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
enable,
|
|
||||||
fields
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const metadataFields = [];
|
|
||||||
const imageFields = [];
|
|
||||||
|
|
||||||
fields.forEach((field) => {
|
|
||||||
if (field.section === 'metadata') {
|
|
||||||
metadataFields.push(field);
|
|
||||||
} else {
|
|
||||||
imageFields.push(field);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
className={styles.metadata}
|
|
||||||
overlayContent={true}
|
|
||||||
onPress={this.onEditMetadataPress}
|
|
||||||
>
|
|
||||||
<div className={styles.name}>
|
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
enable ?
|
|
||||||
<Label kind={kinds.SUCCESS}>
|
|
||||||
{translate('Enabled')}
|
|
||||||
</Label> :
|
|
||||||
<Label
|
|
||||||
kind={kinds.DISABLED}
|
|
||||||
outline={true}
|
|
||||||
>
|
|
||||||
{translate('Disabled')}
|
|
||||||
</Label>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
enable && !!metadataFields.length &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.section}>
|
|
||||||
{translate('Metadata')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
metadataFields.map((field) => {
|
|
||||||
if (!field.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={field.label}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
enable && !!imageFields.length &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.section}>
|
|
||||||
{translate('Images')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
imageFields.map((field) => {
|
|
||||||
if (!field.value) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Label
|
|
||||||
key={field.label}
|
|
||||||
kind={kinds.SUCCESS}
|
|
||||||
>
|
|
||||||
{field.label}
|
|
||||||
</Label>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<EditMetadataModalConnector
|
|
||||||
id={id}
|
|
||||||
isOpen={this.state.isEditMetadataModalOpen}
|
|
||||||
onModalClose={this.onEditMetadataModalClose}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadata.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
name: PropTypes.string.isRequired,
|
|
||||||
enable: PropTypes.bool.isRequired,
|
|
||||||
fields: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Metadata;
|
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import React, { useCallback, useMemo, useState } from 'react';
|
||||||
|
import Card from 'Components/Card';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Field from 'typings/Field';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import EditMetadataModal from './EditMetadataModal';
|
||||||
|
import styles from './Metadata.css';
|
||||||
|
|
||||||
|
interface MetadataProps {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
enable: boolean;
|
||||||
|
fields: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metadata({ id, name, enable, fields }: MetadataProps) {
|
||||||
|
const [isEditMetadataModalOpen, setIsEditMetadataModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const { metadataFields, imageFields } = useMemo(() => {
|
||||||
|
return fields.reduce<{ metadataFields: Field[]; imageFields: Field[] }>(
|
||||||
|
(acc, field) => {
|
||||||
|
if (field.section === 'metadata') {
|
||||||
|
acc.metadataFields.push(field);
|
||||||
|
} else {
|
||||||
|
acc.imageFields.push(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{ metadataFields: [], imageFields: [] }
|
||||||
|
);
|
||||||
|
}, [fields]);
|
||||||
|
|
||||||
|
const handleOpenPress = useCallback(() => {
|
||||||
|
setIsEditMetadataModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
setIsEditMetadataModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
className={styles.metadata}
|
||||||
|
overlayContent={true}
|
||||||
|
onPress={handleOpenPress}
|
||||||
|
>
|
||||||
|
<div className={styles.name}>{name}</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{enable ? (
|
||||||
|
<Label kind={kinds.SUCCESS}>{translate('Enabled')}</Label>
|
||||||
|
) : (
|
||||||
|
<Label kind={kinds.DISABLED} outline={true}>
|
||||||
|
{translate('Disabled')}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enable && metadataFields.length ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.section}>{translate('Metadata')}</div>
|
||||||
|
|
||||||
|
{metadataFields.map((field) => {
|
||||||
|
if (!field.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label key={field.label} kind={kinds.SUCCESS}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{enable && imageFields.length ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.section}>{translate('Images')}</div>
|
||||||
|
|
||||||
|
{imageFields.map((field) => {
|
||||||
|
if (!field.value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label key={field.label} kind={kinds.SUCCESS}>
|
||||||
|
{field.label}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<EditMetadataModal
|
||||||
|
id={id}
|
||||||
|
isOpen={isEditMetadataModalOpen}
|
||||||
|
onModalClose={handleModalClose}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadata;
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import FieldSet from 'Components/FieldSet';
|
|
||||||
import PageSectionContent from 'Components/Page/PageSectionContent';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import Metadata from './Metadata';
|
|
||||||
import styles from './Metadatas.css';
|
|
||||||
|
|
||||||
function Metadatas(props) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FieldSet legend={translate('Metadata')}>
|
|
||||||
<PageSectionContent
|
|
||||||
errorMessage={translate('MetadataLoadError')}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<div className={styles.metadatas}>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<Metadata
|
|
||||||
key={item.id}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</PageSectionContent>
|
|
||||||
</FieldSet>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Metadatas.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Metadatas;
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import MetadataAppState from 'App/State/MetadataAppState';
|
||||||
|
import FieldSet from 'Components/FieldSet';
|
||||||
|
import PageSectionContent from 'Components/Page/PageSectionContent';
|
||||||
|
import { fetchMetadata } from 'Store/Actions/settingsActions';
|
||||||
|
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||||
|
import MetadataType from 'typings/Metadata';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import Metadata from './Metadata';
|
||||||
|
import styles from './Metadatas.css';
|
||||||
|
|
||||||
|
function createMetadatasSelector() {
|
||||||
|
return createSelector(
|
||||||
|
createSortedSectionSelector<MetadataType>(
|
||||||
|
'settings.metadata',
|
||||||
|
sortByProp('name')
|
||||||
|
),
|
||||||
|
(metadata: MetadataAppState) => metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Metadatas() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { isFetching, error, items, ...otherProps } = useSelector(
|
||||||
|
createMetadatasSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchMetadata());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FieldSet legend={translate('Metadata')}>
|
||||||
|
<PageSectionContent
|
||||||
|
isFetching={isFetching}
|
||||||
|
errorMessage={translate('MetadataLoadError')}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.metadatas}>
|
||||||
|
{items.map((item) => {
|
||||||
|
return <Metadata key={item.id} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</PageSectionContent>
|
||||||
|
</FieldSet>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Metadatas;
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchMetadata } from 'Store/Actions/settingsActions';
|
|
||||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
|
||||||
import sortByProp from 'Utilities/Array/sortByProp';
|
|
||||||
import Metadatas from './Metadatas';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createSortedSectionSelector('settings.metadata', sortByProp('name')),
|
|
||||||
(metadata) => metadata
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchMetadata
|
|
||||||
};
|
|
||||||
|
|
||||||
class MetadatasConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.fetchMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Metadatas
|
|
||||||
{...this.props}
|
|
||||||
onConfirmDeleteMetadata={this.onConfirmDeleteMetadata}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MetadatasConnector.propTypes = {
|
|
||||||
fetchMetadata: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(MetadatasConnector);
|
|
||||||
@@ -3,7 +3,7 @@ import PageContent from 'Components/Page/PageContent';
|
|||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import MetadatasConnector from './Metadata/MetadatasConnector';
|
import Metadatas from './Metadata/Metadatas';
|
||||||
import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
|
import MetadataOptionsConnector from './Options/MetadataOptionsConnector';
|
||||||
|
|
||||||
class MetadataSettings extends Component {
|
class MetadataSettings extends Component {
|
||||||
@@ -62,7 +62,7 @@ class MetadataSettings extends Component {
|
|||||||
onChildStateChange={this.onChildStateChange}
|
onChildStateChange={this.onChildStateChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MetadatasConnector />
|
<Metadatas />
|
||||||
</PageContentBody>
|
</PageContentBody>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,14 +19,15 @@ import {
|
|||||||
setReleaseProfileValue,
|
setReleaseProfileValue,
|
||||||
} from 'Store/Actions/Settings/releaseProfiles';
|
} from 'Store/Actions/Settings/releaseProfiles';
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
import selectSettings from 'Store/Selectors/selectSettings';
|
||||||
import { PendingSection } from 'typings/pending';
|
|
||||||
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './EditReleaseProfileModalContent.css';
|
import styles from './EditReleaseProfileModalContent.css';
|
||||||
|
|
||||||
const tagInputDelimiters = ['Tab', 'Enter'];
|
const tagInputDelimiters = ['Tab', 'Enter'];
|
||||||
|
|
||||||
const newReleaseProfile = {
|
const newReleaseProfile: ReleaseProfile = {
|
||||||
|
id: 0,
|
||||||
|
name: '',
|
||||||
enabled: true,
|
enabled: true,
|
||||||
required: [],
|
required: [],
|
||||||
ignored: [],
|
ignored: [],
|
||||||
@@ -41,8 +42,12 @@ function createReleaseProfileSelector(id?: number) {
|
|||||||
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
|
const { items, isFetching, error, isSaving, saveError, pendingChanges } =
|
||||||
releaseProfiles;
|
releaseProfiles;
|
||||||
|
|
||||||
const mapping = id ? items.find((i) => i.id === id) : newReleaseProfile;
|
const mapping = id ? items.find((i) => i.id === id)! : newReleaseProfile;
|
||||||
const settings = selectSettings(mapping, pendingChanges, saveError);
|
const settings = selectSettings<ReleaseProfile>(
|
||||||
|
mapping,
|
||||||
|
pendingChanges,
|
||||||
|
saveError
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -50,7 +55,7 @@ function createReleaseProfileSelector(id?: number) {
|
|||||||
error,
|
error,
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
item: settings.settings as PendingSection<ReleaseProfile>,
|
item: settings.settings,
|
||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -86,10 +86,10 @@ function EditSpecificationModalContent(props) {
|
|||||||
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
<InlineMarkdown data={translate('ConditionUsingRegularExpressions')} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsTutorialLink')} />
|
<InlineMarkdown data={translate('RegularExpressionsTutorialLink', { url: 'https://www.regular-expressions.info/tutorial.html' })} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<InlineMarkdown data={translate('RegularExpressionsCanBeTested')} />
|
<InlineMarkdown data={translate('RegularExpressionsCanBeTested', { url: 'http://regexstorm.net/tester' })} />
|
||||||
</div>
|
</div>
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createAction } from 'redux-actions';
|
|||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
import { sortDirections } from 'Helpers/Props';
|
import { sortDirections } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
|
import sortByProp from 'Utilities/Array/sortByProp';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import naturalExpansion from 'Utilities/String/naturalExpansion';
|
import naturalExpansion from 'Utilities/String/naturalExpansion';
|
||||||
import { set, update, updateItem } from './baseActions';
|
import { set, update, updateItem } from './baseActions';
|
||||||
@@ -30,6 +31,7 @@ export const defaultState = {
|
|||||||
items: [],
|
items: [],
|
||||||
sortKey: 'relativePath',
|
sortKey: 'relativePath',
|
||||||
sortDirection: sortDirections.ASCENDING,
|
sortDirection: sortDirections.ASCENDING,
|
||||||
|
favoriteFolders: [],
|
||||||
recentFolders: [],
|
recentFolders: [],
|
||||||
importMode: 'chooseImportMode',
|
importMode: 'chooseImportMode',
|
||||||
sortPredicates: {
|
sortPredicates: {
|
||||||
@@ -58,6 +60,7 @@ export const defaultState = {
|
|||||||
export const persistState = [
|
export const persistState = [
|
||||||
'interactiveImport.sortKey',
|
'interactiveImport.sortKey',
|
||||||
'interactiveImport.sortDirection',
|
'interactiveImport.sortDirection',
|
||||||
|
'interactiveImport.favoriteFolders',
|
||||||
'interactiveImport.recentFolders',
|
'interactiveImport.recentFolders',
|
||||||
'interactiveImport.importMode'
|
'interactiveImport.importMode'
|
||||||
];
|
];
|
||||||
@@ -73,6 +76,8 @@ export const UPDATE_INTERACTIVE_IMPORT_ITEMS = 'interactiveImport/updateInteract
|
|||||||
export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport';
|
export const CLEAR_INTERACTIVE_IMPORT = 'interactiveImport/clearInteractiveImport';
|
||||||
export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
|
export const ADD_RECENT_FOLDER = 'interactiveImport/addRecentFolder';
|
||||||
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
export const REMOVE_RECENT_FOLDER = 'interactiveImport/removeRecentFolder';
|
||||||
|
export const ADD_FAVORITE_FOLDER = 'interactiveImport/addFavoriteFolder';
|
||||||
|
export const REMOVE_FAVORITE_FOLDER = 'interactiveImport/removeFavoriteFolder';
|
||||||
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
export const SET_INTERACTIVE_IMPORT_MODE = 'interactiveImport/setInteractiveImportMode';
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -86,6 +91,8 @@ export const updateInteractiveImportItems = createAction(UPDATE_INTERACTIVE_IMPO
|
|||||||
export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT);
|
export const clearInteractiveImport = createAction(CLEAR_INTERACTIVE_IMPORT);
|
||||||
export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
|
export const addRecentFolder = createAction(ADD_RECENT_FOLDER);
|
||||||
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
export const removeRecentFolder = createAction(REMOVE_RECENT_FOLDER);
|
||||||
|
export const addFavoriteFolder = createAction(ADD_FAVORITE_FOLDER);
|
||||||
|
export const removeFavoriteFolder = createAction(REMOVE_FAVORITE_FOLDER);
|
||||||
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
export const setInteractiveImportMode = createAction(SET_INTERACTIVE_IMPORT_MODE);
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -264,9 +271,31 @@ export const reducers = createHandleActions({
|
|||||||
return Object.assign({}, state, { recentFolders });
|
return Object.assign({}, state, { recentFolders });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[ADD_FAVORITE_FOLDER]: function(state, { payload }) {
|
||||||
|
const folder = payload.folder;
|
||||||
|
const favoriteFolder = { folder };
|
||||||
|
const favoriteFolders = [...state.favoriteFolders, favoriteFolder].sort(sortByProp('folder'));
|
||||||
|
|
||||||
|
return Object.assign({}, state, { favoriteFolders });
|
||||||
|
},
|
||||||
|
|
||||||
|
[REMOVE_FAVORITE_FOLDER]: function(state, { payload }) {
|
||||||
|
const folder = payload.folder;
|
||||||
|
const favoriteFolders = state.favoriteFolders.reduce((acc, item) => {
|
||||||
|
if (item.folder !== folder) {
|
||||||
|
acc.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return Object.assign({}, state, { favoriteFolders });
|
||||||
|
},
|
||||||
|
|
||||||
[CLEAR_INTERACTIVE_IMPORT]: function(state) {
|
[CLEAR_INTERACTIVE_IMPORT]: function(state) {
|
||||||
const newState = {
|
const newState = {
|
||||||
...defaultState,
|
...defaultState,
|
||||||
|
favoriteFolders: state.favoriteFolders,
|
||||||
recentFolders: state.recentFolders,
|
recentFolders: state.recentFolders,
|
||||||
importMode: state.importMode
|
importMode: state.importMode
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -91,12 +91,8 @@ export const defaultState = {
|
|||||||
genres: function(item, filterValue, type) {
|
genres: function(item, filterValue, type) {
|
||||||
const predicate = filterTypePredicates[type];
|
const predicate = filterTypePredicates[type];
|
||||||
|
|
||||||
let allGenres = [];
|
const allGenres = item.movies.flatMap(({ genres }) => genres);
|
||||||
item.movies.forEach((movie) => {
|
const genres = Array.from(new Set(allGenres));
|
||||||
allGenres = allGenres.concat(movie.genres);
|
|
||||||
});
|
|
||||||
|
|
||||||
const genres = Array.from(new Set(allGenres)).slice(0, 3);
|
|
||||||
|
|
||||||
return predicate(genres, filterValue);
|
return predicate(genres, filterValue);
|
||||||
},
|
},
|
||||||
@@ -138,12 +134,8 @@ export const defaultState = {
|
|||||||
type: filterBuilderTypes.ARRAY,
|
type: filterBuilderTypes.ARRAY,
|
||||||
optionsSelector: function(items) {
|
optionsSelector: function(items) {
|
||||||
const genreList = items.reduce((acc, collection) => {
|
const genreList = items.reduce((acc, collection) => {
|
||||||
let collectionGenres = [];
|
const collectionGenres = collection.movies.flatMap(({ genres }) => genres);
|
||||||
collection.movies.forEach((movie) => {
|
const genres = Array.from(new Set(collectionGenres));
|
||||||
collectionGenres = collectionGenres.concat(movie.genres);
|
|
||||||
});
|
|
||||||
|
|
||||||
const genres = Array.from(new Set(collectionGenres)).slice(0, 3);
|
|
||||||
|
|
||||||
genres.forEach((genre) => {
|
genres.forEach((genre) => {
|
||||||
acc.push({
|
acc.push({
|
||||||
|
|||||||
@@ -201,6 +201,12 @@ export const defaultState = {
|
|||||||
label: () => translate('Protocol'),
|
label: () => translate('Protocol'),
|
||||||
type: filterBuilderTypes.EQUAL,
|
type: filterBuilderTypes.EQUAL,
|
||||||
valueType: filterBuilderValueTypes.PROTOCOL
|
valueType: filterBuilderValueTypes.PROTOCOL
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
label: () => translate('Status'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.QUEUE_STATUS
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import AppState from 'App/State/AppState';
|
import AppState from 'App/State/AppState';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { createMovieSelectorForHook } from './createMovieSelector';
|
import { createMovieSelectorForHook } from './createMovieSelector';
|
||||||
|
|
||||||
function createMovieQualityProfileSelector(movieId: number) {
|
function createMovieQualityProfileSelector(movieId: number) {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.settings.qualityProfiles.items,
|
(state: AppState) => state.settings.qualityProfiles.items,
|
||||||
createMovieSelectorForHook(movieId),
|
createMovieSelectorForHook(movieId),
|
||||||
(qualityProfiles, movie = {} as Movie) => {
|
(qualityProfiles: QualityProfile[], movie = {} as Movie) => {
|
||||||
return qualityProfiles.find(
|
return qualityProfiles.find(
|
||||||
(profile) => profile.id === movie.qualityProfileId
|
(profile) => profile.id === movie.qualityProfileId
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
function getValidationFailures(saveError) {
|
|
||||||
if (!saveError || saveError.status !== 400) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return _.cloneDeep(saveError.responseJSON);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapFailure(failure) {
|
|
||||||
return {
|
|
||||||
message: failure.errorMessage,
|
|
||||||
link: failure.infoLink,
|
|
||||||
detailedMessage: failure.detailedDescription
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectSettings(item, pendingChanges, saveError) {
|
|
||||||
const validationFailures = getValidationFailures(saveError);
|
|
||||||
|
|
||||||
// Merge all settings from the item along with pending
|
|
||||||
// changes to ensure any settings that were not included
|
|
||||||
// with the item are included.
|
|
||||||
const allSettings = Object.assign({}, item, pendingChanges);
|
|
||||||
|
|
||||||
const settings = _.reduce(allSettings, (result, value, key) => {
|
|
||||||
if (key === 'fields') {
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return a flattened value
|
|
||||||
if (key === 'implementationName') {
|
|
||||||
result.implementationName = item[key];
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const setting = {
|
|
||||||
value: item[key],
|
|
||||||
errors: _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === key.toLowerCase() && !failure.isWarning;
|
|
||||||
}), mapFailure),
|
|
||||||
|
|
||||||
warnings: _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === key.toLowerCase() && failure.isWarning;
|
|
||||||
}), mapFailure)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (pendingChanges.hasOwnProperty(key)) {
|
|
||||||
setting.previousValue = setting.value;
|
|
||||||
setting.value = pendingChanges[key];
|
|
||||||
setting.pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
result[key] = setting;
|
|
||||||
return result;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const fields = _.reduce(item.fields, (result, f) => {
|
|
||||||
const field = Object.assign({ pending: false }, f);
|
|
||||||
const hasPendingFieldChange = pendingChanges.fields && pendingChanges.fields.hasOwnProperty(field.name);
|
|
||||||
|
|
||||||
if (hasPendingFieldChange) {
|
|
||||||
field.previousValue = field.value;
|
|
||||||
field.value = pendingChanges.fields[field.name];
|
|
||||||
field.pending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
field.errors = _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && !failure.isWarning;
|
|
||||||
}), mapFailure);
|
|
||||||
|
|
||||||
field.warnings = _.map(_.remove(validationFailures, (failure) => {
|
|
||||||
return failure.propertyName.toLowerCase() === field.name.toLowerCase() && failure.isWarning;
|
|
||||||
}), mapFailure);
|
|
||||||
|
|
||||||
result.push(field);
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (fields.length) {
|
|
||||||
settings.fields = fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
const validationErrors = _.filter(validationFailures, (failure) => {
|
|
||||||
return !failure.isWarning;
|
|
||||||
});
|
|
||||||
|
|
||||||
const validationWarnings = _.filter(validationFailures, (failure) => {
|
|
||||||
return failure.isWarning;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings,
|
|
||||||
validationErrors,
|
|
||||||
validationWarnings,
|
|
||||||
hasPendingChanges: !_.isEmpty(pendingChanges),
|
|
||||||
hasSettings: !_.isEmpty(settings),
|
|
||||||
pendingChanges
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default selectSettings;
|
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { cloneDeep, isEmpty } from 'lodash';
|
||||||
|
import { Error } from 'App/State/AppSectionState';
|
||||||
|
import Field from 'typings/Field';
|
||||||
|
import {
|
||||||
|
Failure,
|
||||||
|
Pending,
|
||||||
|
PendingField,
|
||||||
|
PendingSection,
|
||||||
|
ValidationError,
|
||||||
|
ValidationFailure,
|
||||||
|
ValidationWarning,
|
||||||
|
} from 'typings/pending';
|
||||||
|
|
||||||
|
interface ValidationFailures {
|
||||||
|
errors: ValidationError[];
|
||||||
|
warnings: ValidationWarning[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidationFailures(saveError?: Error): ValidationFailures {
|
||||||
|
if (!saveError || saveError.status !== 400) {
|
||||||
|
return {
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return cloneDeep(saveError.responseJSON as ValidationFailure[]).reduce(
|
||||||
|
(acc: ValidationFailures, failure: ValidationFailure) => {
|
||||||
|
if (failure.isWarning) {
|
||||||
|
acc.warnings.push(failure as ValidationWarning);
|
||||||
|
} else {
|
||||||
|
acc.errors.push(failure as ValidationError);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFailures(failures: ValidationFailure[], key: string) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
for (let i = failures.length - 1; i >= 0; i--) {
|
||||||
|
if (failures[i].propertyName.toLowerCase() === key.toLowerCase()) {
|
||||||
|
result.unshift(mapFailure(failures[i]));
|
||||||
|
|
||||||
|
failures.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapFailure(failure: ValidationFailure): Failure {
|
||||||
|
return {
|
||||||
|
errorMessage: failure.errorMessage,
|
||||||
|
infoLink: failure.infoLink,
|
||||||
|
detailedDescription: failure.detailedDescription,
|
||||||
|
|
||||||
|
// TODO: Remove these renamed properties
|
||||||
|
message: failure.errorMessage,
|
||||||
|
link: failure.infoLink,
|
||||||
|
detailedMessage: failure.detailedDescription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ModelBaseSetting {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
[id: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectSettings<T extends ModelBaseSetting>(
|
||||||
|
item: T,
|
||||||
|
pendingChanges: Partial<ModelBaseSetting>,
|
||||||
|
saveError?: Error
|
||||||
|
) {
|
||||||
|
const { errors, warnings } = getValidationFailures(saveError);
|
||||||
|
|
||||||
|
// Merge all settings from the item along with pending
|
||||||
|
// changes to ensure any settings that were not included
|
||||||
|
// with the item are included.
|
||||||
|
const allSettings = Object.assign({}, item, pendingChanges);
|
||||||
|
|
||||||
|
const settings = Object.keys(allSettings).reduce(
|
||||||
|
(acc: PendingSection<T>, key) => {
|
||||||
|
if (key === 'fields') {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a flattened value
|
||||||
|
if (key === 'implementationName') {
|
||||||
|
acc.implementationName = item[key];
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
const setting: Pending<T> = {
|
||||||
|
value: item[key],
|
||||||
|
pending: false,
|
||||||
|
errors: getFailures(errors, key),
|
||||||
|
warnings: getFailures(warnings, key),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pendingChanges.hasOwnProperty(key)) {
|
||||||
|
setting.previousValue = setting.value;
|
||||||
|
setting.value = pendingChanges[key];
|
||||||
|
setting.pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ts-expect-error - This is a valid key
|
||||||
|
acc[key] = setting;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as PendingSection<T>
|
||||||
|
);
|
||||||
|
|
||||||
|
if ('fields' in item) {
|
||||||
|
const fields =
|
||||||
|
(item.fields as Field[]).reduce((acc: PendingField<T>[], f) => {
|
||||||
|
const field: PendingField<T> = Object.assign(
|
||||||
|
{ pending: false, errors: [], warnings: [] },
|
||||||
|
f
|
||||||
|
);
|
||||||
|
|
||||||
|
if ('fields' in pendingChanges) {
|
||||||
|
const pendingChangesFields = pendingChanges.fields as Record<
|
||||||
|
string,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (pendingChangesFields.hasOwnProperty(field.name)) {
|
||||||
|
field.previousValue = field.value;
|
||||||
|
field.value = pendingChangesFields[field.name];
|
||||||
|
field.pending = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
field.errors = getFailures(errors, field.name);
|
||||||
|
field.warnings = getFailures(warnings, field.name);
|
||||||
|
|
||||||
|
acc.push(field);
|
||||||
|
return acc;
|
||||||
|
}, []) ?? [];
|
||||||
|
|
||||||
|
if (fields.length) {
|
||||||
|
settings.fields = fields;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationErrors = errors;
|
||||||
|
const validationWarnings = warnings;
|
||||||
|
|
||||||
|
return {
|
||||||
|
settings,
|
||||||
|
validationErrors,
|
||||||
|
validationWarnings,
|
||||||
|
hasPendingChanges: !isEmpty(pendingChanges),
|
||||||
|
hasSettings: !isEmpty(settings),
|
||||||
|
pendingChanges,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default selectSettings;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export type Entries<T> = {
|
||||||
|
[K in keyof T]: [K, T[K]];
|
||||||
|
}[keyof T][];
|
||||||
|
|
||||||
|
function getEntries<T extends object>(obj: T): Entries<T> {
|
||||||
|
return Object.entries(obj) as Entries<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getEntries;
|
||||||
@@ -35,7 +35,7 @@ export default function getLanguageName(code: string) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return languageNames.of(code) ?? code;
|
return languageNames.of(code) ?? code;
|
||||||
} catch (error) {
|
} catch {
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function fetchTranslations(): Promise<boolean> {
|
|||||||
translations = data.Strings;
|
translations = data.Strings;
|
||||||
|
|
||||||
resolve(true);
|
resolve(true);
|
||||||
} catch (error) {
|
} catch {
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -27,6 +27,12 @@ export default function translate(
|
|||||||
key: string,
|
key: string,
|
||||||
tokens: Record<string, string | number | boolean> = {}
|
tokens: Record<string, string | number | boolean> = {}
|
||||||
) {
|
) {
|
||||||
|
const { isProduction = true } = window.Radarr;
|
||||||
|
|
||||||
|
if (!isProduction && !(key in translations)) {
|
||||||
|
console.warn(`Missing translation for key: ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
const translation = translations[key] || key;
|
const translation = translations[key] || key;
|
||||||
|
|
||||||
tokens.appName = 'Radarr';
|
tokens.appName = 'Radarr';
|
||||||
|
|||||||
@@ -153,12 +153,15 @@ class CutoffUnmet extends Component {
|
|||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('SearchSelected')}
|
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
isDisabled={!itemsSelected || isSearchingForCutoffUnmetMovies}
|
isDisabled={isSearchingForCutoffUnmetMovies}
|
||||||
onPress={this.onSearchSelectedPress}
|
isSpinning={isSearchingForCutoffUnmetMovies}
|
||||||
|
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllCutoffUnmetPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||||
iconName={icons.MONITORED}
|
iconName={icons.MONITORED}
|
||||||
@@ -166,18 +169,6 @@ class CutoffUnmet extends Component {
|
|||||||
isSpinning={isSaving}
|
isSpinning={isSaving}
|
||||||
onPress={this.onToggleSelectedPress}
|
onPress={this.onToggleSelectedPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('SearchAll')}
|
|
||||||
iconName={icons.SEARCH}
|
|
||||||
isDisabled={!items.length}
|
|
||||||
isSpinning={isSearchingForCutoffUnmetMovies}
|
|
||||||
onPress={this.onSearchAllCutoffUnmetPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.wanted.cutoffUnmet,
|
(state) => state.wanted.cutoffUnmet,
|
||||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
|
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_MOVIES_SEARCH),
|
||||||
(cutoffUnmet, isSearchingForCutoffUnmetMovies) => {
|
createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
|
||||||
|
(cutoffUnmet, isSearchingForCutoffUnmetMovies, isSearchingForSelectedCutoffUnmetMovies) => {
|
||||||
return {
|
return {
|
||||||
isSearchingForCutoffUnmetMovies,
|
isSearchingForCutoffUnmetMovies: isSearchingForCutoffUnmetMovies || isSearchingForSelectedCutoffUnmetMovies,
|
||||||
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
|
isSaving: cutoffUnmet.items.filter((m) => m.isSaving).length > 1,
|
||||||
...cutoffUnmet
|
...cutoffUnmet
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import movieEntities from 'Movie/movieEntities';
|
|||||||
import MovieSearchCell from 'Movie/MovieSearchCell';
|
import MovieSearchCell from 'Movie/MovieSearchCell';
|
||||||
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
import MovieStatusConnector from 'Movie/MovieStatusConnector';
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
import MovieFileLanguageConnector from 'MovieFile/MovieFileLanguageConnector';
|
import MovieFileLanguages from 'MovieFile/MovieFileLanguages';
|
||||||
import styles from './CutoffUnmetRow.css';
|
import styles from './CutoffUnmetRow.css';
|
||||||
|
|
||||||
function CutoffUnmetRow(props) {
|
function CutoffUnmetRow(props) {
|
||||||
@@ -104,7 +104,7 @@ function CutoffUnmetRow(props) {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.languages}
|
className={styles.languages}
|
||||||
>
|
>
|
||||||
<MovieFileLanguageConnector
|
<MovieFileLanguages
|
||||||
movieFileId={movieFileId}
|
movieFileId={movieFileId}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|||||||
@@ -159,12 +159,15 @@ class Missing extends Component {
|
|||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection>
|
<PageToolbarSection>
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('SearchSelected')}
|
label={itemsSelected ? translate('SearchSelected') : translate('SearchAll')}
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
isDisabled={!itemsSelected || isSearchingForMissingMovies}
|
isSpinning={isSearchingForMissingMovies}
|
||||||
onPress={this.onSearchSelectedPress}
|
isDisabled={isSearchingForMissingMovies}
|
||||||
|
onPress={itemsSelected ? this.onSearchSelectedPress : this.onSearchAllMissingPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
label={isShowingMonitored ? translate('UnmonitorSelected') : translate('MonitorSelected')}
|
||||||
iconName={icons.MONITORED}
|
iconName={icons.MONITORED}
|
||||||
@@ -175,16 +178,6 @@ class Missing extends Component {
|
|||||||
|
|
||||||
<PageToolbarSeparator />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('SearchAll')}
|
|
||||||
iconName={icons.SEARCH}
|
|
||||||
isDisabled={!items.length}
|
|
||||||
isSpinning={isSearchingForMissingMovies}
|
|
||||||
onPress={this.onSearchAllMissingPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('ManualImport')}
|
label={translate('ManualImport')}
|
||||||
iconName={icons.INTERACTIVE}
|
iconName={icons.INTERACTIVE}
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.wanted.missing,
|
(state) => state.wanted.missing,
|
||||||
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
|
createCommandExecutingSelector(commandNames.MISSING_MOVIES_SEARCH),
|
||||||
(missing, isSearchingForMissingMovies) => {
|
createCommandExecutingSelector(commandNames.MOVIE_SEARCH),
|
||||||
|
(missing, isSearchingForMissingMovies, isSearchingForSelectedMissingMovies) => {
|
||||||
return {
|
return {
|
||||||
isSearchingForMissingMovies,
|
isSearchingForMissingMovies: isSearchingForMissingMovies || isSearchingForSelectedMissingMovies,
|
||||||
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
|
isSaving: missing.items.filter((m) => m.isSaving).length > 1,
|
||||||
...missing
|
...missing
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createBrowserHistory } from 'history';
|
import { createBrowserHistory } from 'history';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render } from 'react-dom';
|
import { createRoot } from 'react-dom/client';
|
||||||
import createAppStore from 'Store/createAppStore';
|
import createAppStore from 'Store/createAppStore';
|
||||||
import App from './App/App';
|
import App from './App/App';
|
||||||
|
|
||||||
@@ -9,9 +9,8 @@ import 'Diag/ConsoleApi';
|
|||||||
export async function bootstrap() {
|
export async function bootstrap() {
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
const store = createAppStore(history);
|
const store = createAppStore(history);
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
|
||||||
render(
|
const root = createRoot(container!); // createRoot(container!) if you use TypeScript
|
||||||
<App store={store} history={history} />,
|
root.render(<App store={store} history={history} />);
|
||||||
document.getElementById('root')
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user