mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
425 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 470963921d | |||
| 36f9ec4ea7 | |||
| 9df2368601 | |||
| e7d76350ec | |||
| fd3828ff5d | |||
| 368e1fead8 | |||
| 5b357faf16 | |||
| 3f35b7c782 | |||
| 7d29deb93c | |||
| d0bfdce9c5 | |||
| 5d0cd78667 | |||
| afbe0ebcd4 | |||
| bfbb7532a2 | |||
| c92d8c08f1 | |||
| 358ce92f85 | |||
| 3ec5a4b78a | |||
| cb59ce891a | |||
| 4d3d46d796 | |||
| 0941e51d27 | |||
| ff393a3f65 | |||
| f5faf52469 | |||
| b5b4d4b971 | |||
| 873299701b | |||
| d14cca30d7 | |||
| 5af61b5900 | |||
| a10759c7e9 | |||
| ac2d92007e | |||
| 09cfdc3fa2 | |||
| 04f26dbff7 | |||
| 159f5df8cc | |||
| b823ad8e65 | |||
| cc8bffc272 | |||
| e0b93a03fd | |||
| f7f5837d49 | |||
| c3ee8b3c90 | |||
| 4de78e3bab | |||
| 426538c8af | |||
| c82404c75b | |||
| 9bee9841c1 | |||
| 010959d915 | |||
| a600728916 | |||
| bbfb8c7cc2 | |||
| 32418ea521 | |||
| 2c5c99e9b7 | |||
| a5e5a63e45 | |||
| 31b44d2c2e | |||
| da8e8a12de | |||
| 6506c97ce1 | |||
| 5303a1992c | |||
| 042308c319 | |||
| 2e97e09f44 | |||
| ccfb9c0dad | |||
| b655d97e9e | |||
| 3afcb91db6 | |||
| 704e2d6176 | |||
| 8314c37b1d | |||
| c2c3dfe917 | |||
| c58a9b3f2c | |||
| 65a532a7fd | |||
| 704d920dab | |||
| 025cb0788f | |||
| 82c21d8bb1 | |||
| 96f973c961 | |||
| a1ed440945 | |||
| 8caa839d99 | |||
| 9228e5dea0 | |||
| 371ac0921d | |||
| 937557e214 | |||
| 7fdaf41325 | |||
| 577eb4f4ca | |||
| 311f41b306 | |||
| 78f3b1f403 | |||
| 4dc02dcb80 | |||
| 2f649e413d | |||
| 107ddd3826 | |||
| dfdd2cba99 | |||
| c57d68c3dd | |||
| 6cc02b734e | |||
| c5fa09dd86 | |||
| 29d59315b2 | |||
| 981a3c2db3 | |||
| 3f2ea56bf9 | |||
| 1679ed1327 | |||
| 69a1c1b21b | |||
| 5bd51832a0 | |||
| 52a69b662d | |||
| 7e34d89069 | |||
| b0024b28a5 | |||
| ae5450f75d | |||
| 1d1aca1a04 | |||
| 3a55316ada | |||
| 9ef7c2a0b4 | |||
| e759f3fd0b | |||
| 03429db877 | |||
| bb5f421e38 | |||
| 7dd3ed815a | |||
| cc56482819 | |||
| 40f41847fd | |||
| 8485fc8c75 | |||
| f3026df65d | |||
| cfd25e974f | |||
| c52f9c5ec4 | |||
| b91517afd5 | |||
| ee8aaadb29 | |||
| 0694f2fa76 | |||
| 2c81f3be0f | |||
| 8fb2f64e98 | |||
| efd2b80e10 | |||
| a9bbe06966 | |||
| 4c6f80b308 | |||
| c8299f7e57 | |||
| 445babbca8 | |||
| e5137d13e9 | |||
| fb8f8f4dd3 | |||
| 2b8ca4746a | |||
| 9231a0e526 | |||
| 9fa75f0539 | |||
| 76b5568129 | |||
| 27efe506a7 | |||
| d9be54575a | |||
| a825b96518 | |||
| 221b7a4300 | |||
| 1ac784e323 | |||
| aae34f4c43 | |||
| 7219648fea | |||
| b7be80744c | |||
| 29ca18d3f3 | |||
| d9704a999d | |||
| a23983032a | |||
| 99d68cfd91 | |||
| 9c009a84f2 | |||
| e8ca64fabc | |||
| c821541a2f | |||
| c10aadcc7b | |||
| 4a2202ed7f | |||
| 78c009d6fa | |||
| e03289abe7 | |||
| da2ce10c68 | |||
| 6d34f2afb1 | |||
| 49b0c9639c | |||
| c281e68b9f | |||
| 740d3ce88c | |||
| ad7b85f76d | |||
| 3aa93e7946 | |||
| bc08b0b2e1 | |||
| 107f843303 | |||
| 16b6997b14 | |||
| a5bcac5de9 | |||
| 1e10d569c8 | |||
| 74d2259f67 | |||
| 6e68a91922 | |||
| a962de776b | |||
| e8afde2e90 | |||
| 4633a834f3 | |||
| cd021961f0 | |||
| 456ea3d57c | |||
| d09fa6f880 | |||
| bcd4fe1f08 | |||
| 8efce68922 | |||
| 4b3c29ed93 | |||
| 7ea9161779 | |||
| f5c66c5093 | |||
| a3515db9f7 | |||
| d4bb318253 | |||
| 64e865f296 | |||
| 982f9062bd | |||
| 48075e33ac | |||
| 91f08a83cd | |||
| 886db23c58 | |||
| b646386e77 | |||
| 4aa259a666 | |||
| 35f1a61bf8 | |||
| 1d855aed00 | |||
| f7da5b0866 | |||
| 682cc70acf | |||
| 9d624b07ce | |||
| 2afb41498d | |||
| a0679fcf11 | |||
| df4a69ac02 | |||
| 2c8d8ff2d6 | |||
| 0593568065 | |||
| 25aa719ad6 | |||
| 3ab61a2fee | |||
| 954a040d6e | |||
| 905b23618a | |||
| 8decd5d8e1 | |||
| 8b5b177d16 | |||
| e6c6fceff8 | |||
| dcc8b28a07 | |||
| 02baf4d7a4 | |||
| 22ec1fe492 | |||
| a7dbdadd21 | |||
| 93581e4a2f | |||
| 4c8da09df6 | |||
| 89666175a6 | |||
| 7a33e156a3 | |||
| c7c07404b0 | |||
| abeeee9363 | |||
| 23c30734d2 | |||
| 939e45e646 | |||
| 16ceba2392 | |||
| 94d620d878 | |||
| ee0db93a0a | |||
| f815b31c33 | |||
| c078191b3d | |||
| 653b358fd3 | |||
| 6a7ed22b44 | |||
| 779292490a | |||
| e4e96fc7f9 | |||
| 049bf7715e | |||
| df4dfaac0b | |||
| 89c96b0a80 | |||
| 7db12b6e58 | |||
| 28dee7bc01 | |||
| 8ec60eb0a6 | |||
| 102849a697 | |||
| 95da7d7b47 | |||
| 22b5739967 | |||
| cfba047d80 | |||
| 576d404e70 | |||
| 5959d4e51a | |||
| 2aca6c6e1d | |||
| e8bbe0ee9f | |||
| 66332a110a | |||
| 36c66deb4b | |||
| edec432244 | |||
| 554e15d438 | |||
| 553645a07c | |||
| 7de7e83c5b | |||
| b7a46bedb0 | |||
| 0925769377 | |||
| 72244362fe | |||
| c6526c34e9 | |||
| efa2913dbc | |||
| 35c22a4ffa | |||
| 66d96e21da | |||
| 36d4e9e6cd | |||
| 7189d7b15c | |||
| 6e80113987 | |||
| bb8a0dda63 | |||
| 525ed65687 | |||
| 3fbccc6af3 | |||
| 8e10eecfac | |||
| a3b1512552 | |||
| d375b5ffbe | |||
| 884abc0368 | |||
| f8da7aae03 | |||
| c165118d4d | |||
| b3dd571a92 | |||
| dd900eb739 | |||
| 66aae0c91c | |||
| d888a0a2b3 | |||
| cb5416a18c | |||
| 7977e0be05 | |||
| cd836fef38 | |||
| b0bfbe767c | |||
| 528b93dabe | |||
| 1edcbee5e1 | |||
| 8853dced9f | |||
| c7aa1bae5e | |||
| 405ae77070 | |||
| 6236bc9b4f | |||
| 743c977e5b | |||
| c0e5646f07 | |||
| 10094b4e66 | |||
| d923406f08 | |||
| 69a9c72286 | |||
| 55b9477a01 | |||
| 6b81f92137 | |||
| 3ceda1bcda | |||
| f1f1921517 | |||
| af0c96538a | |||
| 3d52f45b6a | |||
| d4715f119d | |||
| d58135bf17 | |||
| b452c10da3 | |||
| f6b364725d | |||
| 99f6be3f3d | |||
| c2ac49a873 | |||
| 0e24a3e8bc | |||
| 18032cc83b | |||
| 927eb38945 | |||
| 5fac348613 | |||
| 7ba9603449 | |||
| e36de8ab8d | |||
| f8704a1655 | |||
| f507d5154e | |||
| 5f03e7142a | |||
| c0ebbee7c9 | |||
| 4051cf3d80 | |||
| 9876ed64e2 | |||
| 2f26974ecc | |||
| 25f66a3029 | |||
| 0e25b2708c | |||
| 410870d21e | |||
| a64d931904 | |||
| f0a9e76cfc | |||
| 6f23c465ee | |||
| af60cca9ae | |||
| d34d23a052 | |||
| 0a0da42543 | |||
| e5419f6f06 | |||
| 88d9c08f1a | |||
| 6b4259757c | |||
| f1d7c56d94 | |||
| c81b2e80ee | |||
| 5efefd804b | |||
| 38f9543526 | |||
| aae68e681e | |||
| 1d21bbf78f | |||
| 99c3c8ce5b | |||
| 85171e40a5 | |||
| 86b656d323 | |||
| 5ae5d1043a | |||
| b801aa0935 | |||
| b2b5aa1f79 | |||
| 8c6ba9a543 | |||
| 4e024c51d3 | |||
| e4106f0ede | |||
| 9032ac20ff | |||
| 23fce4bf2e | |||
| 64fd8552f8 | |||
| e016410c10 | |||
| bea943adf8 | |||
| 9780d20f8a | |||
| 62722d45b0 | |||
| 27dd8e8cd5 | |||
| 6c47ede76b | |||
| 7b9562bb38 | |||
| 8b0b7c1cb0 | |||
| 7ebd341cd6 | |||
| 6c85f166ff | |||
| 45aabce107 | |||
| 0caa793df4 | |||
| 9a107cc8d7 | |||
| a6d727fe2a | |||
| 01a53d3624 | |||
| 348c29c9d7 | |||
| 64739712c6 | |||
| 6ac9cca953 | |||
| a2b38c5b7d | |||
| 3cc4105d71 | |||
| 3449a5d3fe | |||
| 5bac157d36 | |||
| 114d260f42 | |||
| 617b9c5d35 | |||
| ba4ccbb0bd | |||
| b845268b3d | |||
| 0fee552074 | |||
| 828b994ef4 | |||
| 7952fd325b | |||
| 4b4e598b67 | |||
| 71ccebd0f5 | |||
| 2607c67912 | |||
| a626b4f3c4 | |||
| 1526bf29f4 | |||
| 2194772736 | |||
| cd490d6334 | |||
| ff609848d8 | |||
| 15b6f7212d | |||
| af06a9f70d | |||
| c3fa440cf8 | |||
| 0411d66520 | |||
| 179637fe8b | |||
| 09b4bf15cf | |||
| ea86d14ca7 | |||
| 2429dd91c6 | |||
| a752476cdb | |||
| 50ce480abf | |||
| 0ef6e56e5d | |||
| 12d5014125 | |||
| c8301d425c | |||
| b1df9b2401 | |||
| ff09da3a69 | |||
| 3b9bd696fb | |||
| 9ab3e6bab7 | |||
| 86f4f86a0a | |||
| 40d95a04e3 | |||
| ca724836ce | |||
| 10e3964111 | |||
| b22a86e1d7 | |||
| 5976d66511 | |||
| b4eff4d4f9 | |||
| 1414a09111 | |||
| b30efd0c62 | |||
| def6950db4 | |||
| f23c2dbaba | |||
| 186e9cdd23 | |||
| 394f34eb2a | |||
| d9f508280d | |||
| b5505800de | |||
| 48a79eb7d3 | |||
| b42f7e09f9 | |||
| 8f507ac726 | |||
| 06d54e0ec2 | |||
| 3708d58847 | |||
| 0049ccd39f | |||
| ab8a2d190e | |||
| 25bb52b206 | |||
| 63c6f70e67 | |||
| 79cd6269f4 | |||
| 879c872179 | |||
| d4993cf69b | |||
| 781e0c9d1c | |||
| c946ed83f9 | |||
| 9aecf94e8e | |||
| 234e23eb47 | |||
| 748d888520 | |||
| 00d50a030c | |||
| 017fa5ad80 | |||
| d5fb1c55c6 | |||
| 1be8385c41 | |||
| 6f26c55a1b | |||
| 2d2de7f76b | |||
| 2c9292c249 | |||
| 6747b74271 | |||
| 13f10906f1 | |||
| 25d08a67e4 | |||
| aadefbe3b0 | |||
| d99a7e9b8a | |||
| dc29526961 | |||
| f8e47fbdc7 | |||
| 56a7725e52 | |||
| abf1b9d6cf | |||
| dd90bf53dd |
@@ -6,7 +6,7 @@
|
|||||||
"features": {
|
"features": {
|
||||||
"ghcr.io/devcontainers/features/node:1": {
|
"ghcr.io/devcontainers/features/node:1": {
|
||||||
"nodeGypDependencies": true,
|
"nodeGypDependencies": true,
|
||||||
"version": "16",
|
"version": "20",
|
||||||
"nvmVersion": "latest"
|
"nvmVersion": "latest"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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-2025
|
||||||
|
|||||||
+21
-11
@@ -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.0'
|
majorVersion: '5.26.0'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
@@ -19,7 +19,7 @@ variables:
|
|||||||
nodeVersion: '20.X'
|
nodeVersion: '20.X'
|
||||||
innoVersion: '6.2.2'
|
innoVersion: '6.2.2'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
linuxImage: 'ubuntu-20.04'
|
linuxImage: 'ubuntu-22.04'
|
||||||
macImage: 'macOS-13'
|
macImage: 'macOS-13'
|
||||||
|
|
||||||
trigger:
|
trigger:
|
||||||
@@ -481,6 +481,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: ne(variables['testName'], 'freebsd-x64')
|
||||||
|
|
||||||
- job: Unit_Docker
|
- job: Unit_Docker
|
||||||
displayName: Unit Docker
|
displayName: Unit Docker
|
||||||
@@ -540,7 +541,8 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Unit Tests'
|
testRunTitle: '$(testName) Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres14
|
- job: Unit_LinuxCore_Postgres14
|
||||||
displayName: Unit Native LinuxCore with Postgres14 Database
|
displayName: Unit Native LinuxCore with Postgres14 Database
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
@@ -596,6 +598,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- job: Unit_LinuxCore_Postgres15
|
- job: Unit_LinuxCore_Postgres15
|
||||||
displayName: Unit Native LinuxCore with Postgres15 Database
|
displayName: Unit Native LinuxCore with Postgres15 Database
|
||||||
@@ -652,6 +655,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
|
|
||||||
- stage: Integration
|
- stage: Integration
|
||||||
displayName: Integration
|
displayName: Integration
|
||||||
@@ -734,6 +738,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Integration Tests'
|
testRunTitle: '$(testName) Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_LinuxCore_Postgres14
|
- job: Integration_LinuxCore_Postgres14
|
||||||
@@ -796,6 +801,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
|
|
||||||
@@ -859,6 +865,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_FreeBSD
|
- job: Integration_FreeBSD
|
||||||
@@ -905,6 +912,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: 'FreeBSD Integration Tests'
|
testRunTitle: 'FreeBSD Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: false
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- job: Integration_Docker
|
- job: Integration_Docker
|
||||||
@@ -974,6 +982,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(testName) Integration Tests'
|
testRunTitle: '$(testName) Integration Tests'
|
||||||
failTaskOnFailedTests: true
|
failTaskOnFailedTests: true
|
||||||
|
failTaskOnMissingResultsFile: true
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- stage: Automation
|
- stage: Automation
|
||||||
@@ -1055,6 +1064,7 @@ stages:
|
|||||||
testResultsFiles: '**/TestResult.xml'
|
testResultsFiles: '**/TestResult.xml'
|
||||||
testRunTitle: '$(osName) Automation Tests'
|
testRunTitle: '$(osName) Automation Tests'
|
||||||
failTaskOnFailedTests: $(failBuild)
|
failTaskOnFailedTests: $(failBuild)
|
||||||
|
failTaskOnMissingResultsFile: $(failBuild)
|
||||||
displayName: Publish Test Results
|
displayName: Publish Test Results
|
||||||
|
|
||||||
- stage: Analyze
|
- stage: Analyze
|
||||||
@@ -1116,20 +1126,20 @@ stages:
|
|||||||
vmImage: ${{ variables.windowsImage }}
|
vmImage: ${{ variables.windowsImage }}
|
||||||
steps:
|
steps:
|
||||||
- checkout: self # Need history for Sonar analysis
|
- checkout: self # Need history for Sonar analysis
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
env:
|
env:
|
||||||
SONAR_SCANNER_OPTS: ''
|
SONAR_SCANNER_OPTS: ''
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'radarr'
|
organization: 'radarr'
|
||||||
scannerMode: 'CLI'
|
scannerMode: 'cli'
|
||||||
configMode: 'manual'
|
configMode: 'manual'
|
||||||
cliProjectKey: 'Radarr_Radarr.UI'
|
cliProjectKey: 'Radarr_Radarr.UI'
|
||||||
cliProjectName: 'RadarrUI'
|
cliProjectName: 'RadarrUI'
|
||||||
cliProjectVersion: '$(radarrVersion)'
|
cliProjectVersion: '$(radarrVersion)'
|
||||||
cliSources: './frontend'
|
cliSources: './frontend'
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
|
|
||||||
- job: Api_Docs
|
- job: Api_Docs
|
||||||
displayName: API Docs
|
displayName: API Docs
|
||||||
dependsOn: Prepare
|
dependsOn: Prepare
|
||||||
@@ -1205,12 +1215,12 @@ stages:
|
|||||||
submodules: true
|
submodules: true
|
||||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||||
displayName: Enable Windows Test Service
|
displayName: Enable Windows Test Service
|
||||||
- task: SonarCloudPrepare@2
|
- task: SonarCloudPrepare@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
inputs:
|
inputs:
|
||||||
SonarCloud: 'SonarCloud'
|
SonarCloud: 'SonarCloud'
|
||||||
organization: 'radarr'
|
organization: 'radarr'
|
||||||
scannerMode: 'MSBuild'
|
scannerMode: 'dotnet'
|
||||||
projectKey: 'Radarr_Radarr'
|
projectKey: 'Radarr_Radarr'
|
||||||
projectName: 'Radarr'
|
projectName: 'Radarr'
|
||||||
projectVersion: '$(radarrVersion)'
|
projectVersion: '$(radarrVersion)'
|
||||||
@@ -1223,10 +1233,10 @@ stages:
|
|||||||
./build.sh --backend -f net6.0 -r win-x64
|
./build.sh --backend -f net6.0 -r win-x64
|
||||||
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
|
||||||
displayName: Coverage Unit Tests
|
displayName: Coverage Unit Tests
|
||||||
- task: SonarCloudAnalyze@2
|
- task: SonarCloudAnalyze@3
|
||||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||||
displayName: Publish SonarCloud Results
|
displayName: Publish SonarCloud Results
|
||||||
- task: reportgenerator@5
|
- task: reportgenerator@5.3.11
|
||||||
displayName: Generate Coverage Report
|
displayName: Generate Coverage Report
|
||||||
inputs:
|
inputs:
|
||||||
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
input1 = """Prometheus.Special.Edition.Fan Edit.2012..BRRip.x264.AAC-m2g
|
|
||||||
Star Wars Episode IV - A New Hope (Despecialized) 1999.mkv
|
|
||||||
Prometheus.(Special.Edition.Remastered).2012.[Bluray-1080p].mkv
|
|
||||||
Prometheus Extended 2012
|
|
||||||
Prometheus Extended Directors Cut Fan Edit 2012
|
|
||||||
Prometheus Director's Cut 2012
|
|
||||||
Prometheus Directors Cut 2012
|
|
||||||
Prometheus.(Extended.Theatrical.Version.IMAX).BluRay.1080p.2012.asdf
|
|
||||||
2001 A Space Odyssey Director's Cut (1968).mkv
|
|
||||||
2001: A Space Odyssey (Extended Directors Cut FanEdit) Bluray 1080p 1968
|
|
||||||
A Fake Movie 2035 Directors 2012.mkv
|
|
||||||
Blade Runner Director's Cut 2049.mkv
|
|
||||||
Prometheus 50th Anniversary Edition 2012.mkv
|
|
||||||
Movie 2in1 2012.mkv
|
|
||||||
Movie IMAX 2012.mkv"""
|
|
||||||
|
|
||||||
output1 = """Special.Edition.Fan Edit BRRip.x264.AAC-m2g
|
|
||||||
Despecialized mkv
|
|
||||||
Special.Edition.Remastered Bluray-1080p].mkv
|
|
||||||
Extended mkv
|
|
||||||
Extended Directors Cut Fan Edit mkv
|
|
||||||
Director's Cut mkv
|
|
||||||
Directors Cut mkv
|
|
||||||
Extended.Theatrical.Version.IMAX asdf
|
|
||||||
Director's Cut mkv
|
|
||||||
Extended Directors Cut FanEdit mkv
|
|
||||||
Directors mkv
|
|
||||||
Director's Cut mkv
|
|
||||||
50th Anniversary Edition mkv
|
|
||||||
2in1 mkv
|
|
||||||
IMAX mkv"""
|
|
||||||
|
|
||||||
inputs = input1.split("\n")
|
|
||||||
outputs = output1.split("\n")
|
|
||||||
real_o = []
|
|
||||||
for output in outputs:
|
|
||||||
real_o.append(output.split(" ")[0].replace(".", " ").strip())
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
for inp in inputs:
|
|
||||||
o = real_o[count]
|
|
||||||
print "[TestCase(\"{0}\", \"{1}\")]".format(inp, o)
|
|
||||||
count += 1
|
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
FRAMEWORK="net6.0"
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
|
ARCHITECTURE="${2:-x64}"
|
||||||
|
|
||||||
if [ "$PLATFORM" = "Windows" ]; then
|
if [ "$PLATFORM" = "Windows" ]; then
|
||||||
RUNTIME="win-x64"
|
RUNTIME="win-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Linux" ]; then
|
elif [ "$PLATFORM" = "Linux" ]; then
|
||||||
RUNTIME="linux-x64"
|
RUNTIME="linux-$ARCHITECTURE"
|
||||||
elif [ "$PLATFORM" = "Mac" ]; then
|
elif [ "$PLATFORM" = "Mac" ]; then
|
||||||
RUNTIME="osx-x64"
|
RUNTIME="osx-$ARCHITECTURE"
|
||||||
else
|
else
|
||||||
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
|
echo "Platform must be provided as first argument: Windows, Linux or Mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -35,7 +40,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
|
|||||||
dotnet new tool-manifest
|
dotnet new tool-manifest
|
||||||
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
|
||||||
|
|
||||||
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/$application" v3 &
|
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
|
||||||
|
|
||||||
sleep 45
|
sleep 45
|
||||||
|
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ module.exports = {
|
|||||||
'no-undef-init': 'off',
|
'no-undef-init': 'off',
|
||||||
'no-undefined': 'off',
|
'no-undefined': 'off',
|
||||||
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
'no-unused-vars': ['error', { args: 'none', ignoreRestSiblings: true }],
|
||||||
'no-use-before-define': 'error',
|
|
||||||
|
|
||||||
// Node.js and CommonJS
|
// Node.js and CommonJS
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ module.exports = (env) => {
|
|||||||
const srcFolder = path.join(frontendFolder, 'src');
|
const srcFolder = path.join(frontendFolder, 'src');
|
||||||
const isProduction = !!env.production;
|
const isProduction = !!env.production;
|
||||||
const isProfiling = isProduction && !!env.profile;
|
const isProfiling = isProduction && !!env.profile;
|
||||||
const inlineWebWorkers = 'no-fallback';
|
|
||||||
|
|
||||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||||
|
|
||||||
@@ -26,6 +25,7 @@ module.exports = (env) => {
|
|||||||
const config = {
|
const config = {
|
||||||
mode: isProduction ? 'production' : 'development',
|
mode: isProduction ? 'production' : 'development',
|
||||||
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
devtool: isProduction ? 'source-map' : 'eval-source-map',
|
||||||
|
target: 'web',
|
||||||
|
|
||||||
stats: {
|
stats: {
|
||||||
children: false
|
children: false
|
||||||
@@ -133,6 +133,12 @@ module.exports = (env) => {
|
|||||||
{
|
{
|
||||||
source: 'frontend/src/Content/robots.txt',
|
source: 'frontend/src/Content/robots.txt',
|
||||||
destination: path.join(distFolder, 'Content/robots.txt')
|
destination: path.join(distFolder, 'Content/robots.txt')
|
||||||
|
},
|
||||||
|
|
||||||
|
// manifest.json and browserconfig.xml
|
||||||
|
{
|
||||||
|
source: 'frontend/src/Content/*.(json|xml)',
|
||||||
|
destination: path.join(distFolder, 'Content')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -153,16 +159,6 @@ module.exports = (env) => {
|
|||||||
|
|
||||||
module: {
|
module: {
|
||||||
rules: [
|
rules: [
|
||||||
{
|
|
||||||
test: /\.worker\.js$/,
|
|
||||||
use: {
|
|
||||||
loader: 'worker-loader',
|
|
||||||
options: {
|
|
||||||
filename: '[name].js',
|
|
||||||
inline: inlineWebWorkers
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
test: [/\.jsx?$/, /\.tsx?$/],
|
test: [/\.jsx?$/, /\.tsx?$/],
|
||||||
exclude: /(node_modules|JsLibraries)/,
|
exclude: /(node_modules|JsLibraries)/,
|
||||||
@@ -180,7 +176,7 @@ module.exports = (env) => {
|
|||||||
loose: true,
|
loose: true,
|
||||||
debug: false,
|
debug: false,
|
||||||
useBuiltIns: 'entry',
|
useBuiltIns: 'entry',
|
||||||
corejs: 3
|
corejs: '3.42'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ function Blocklist() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
const handleFilterSelect = useCallback(
|
||||||
(selectedFilterKey: string) => {
|
(selectedFilterKey: string | number) => {
|
||||||
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
dispatch(setBlocklistFilter({ selectedFilterKey }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|||||||
@@ -1,354 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
|
||||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './HistoryDetails.css';
|
|
||||||
|
|
||||||
function HistoryDetails(props) {
|
|
||||||
const {
|
|
||||||
eventType,
|
|
||||||
sourceTitle,
|
|
||||||
data,
|
|
||||||
downloadId,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
if (eventType === 'grabbed') {
|
|
||||||
const {
|
|
||||||
indexer,
|
|
||||||
releaseGroup,
|
|
||||||
movieMatchType,
|
|
||||||
customFormatScore,
|
|
||||||
nzbInfoUrl,
|
|
||||||
downloadClient,
|
|
||||||
downloadClientName,
|
|
||||||
age,
|
|
||||||
ageHours,
|
|
||||||
ageMinutes,
|
|
||||||
publishedDate
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
indexer ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Indexer')}
|
|
||||||
data={indexer}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
releaseGroup ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ReleaseGroup')}
|
|
||||||
data={releaseGroup}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
movieMatchType ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('MovieMatchType')}
|
|
||||||
data={movieMatchType}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
nzbInfoUrl ?
|
|
||||||
<span>
|
|
||||||
<DescriptionListItemTitle>
|
|
||||||
{translate('InfoUrl')}
|
|
||||||
</DescriptionListItemTitle>
|
|
||||||
|
|
||||||
<DescriptionListItemDescription>
|
|
||||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadClientNameInfo ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DownloadClient')}
|
|
||||||
data={downloadClientNameInfo}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
indexer ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('AgeWhenGrabbed')}
|
|
||||||
data={formatAge(age, ageHours, ageMinutes)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
publishedDate ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('PublishedDate')}
|
|
||||||
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFailed') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadFolderImported') {
|
|
||||||
const {
|
|
||||||
customFormatScore,
|
|
||||||
droppedPath,
|
|
||||||
importedPath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
droppedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Source')}
|
|
||||||
data={droppedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
importedPath ?
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('ImportedTo')}
|
|
||||||
data={importedPath}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'movieFileDeleted') {
|
|
||||||
const {
|
|
||||||
reason,
|
|
||||||
customFormatScore
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
let reasonMessage = '';
|
|
||||||
|
|
||||||
switch (reason) {
|
|
||||||
case 'Manual':
|
|
||||||
reasonMessage = translate('DeletedReasonManual');
|
|
||||||
break;
|
|
||||||
case 'MissingFromDisk':
|
|
||||||
reasonMessage = translate('DeletedReasonMissingFromDisk');
|
|
||||||
break;
|
|
||||||
case 'Upgrade':
|
|
||||||
reasonMessage = translate('DeletedReasonUpgrade');
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
reasonMessage = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Reason')}
|
|
||||||
data={reasonMessage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
customFormatScore && customFormatScore !== '0' ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('CustomFormatScore')}
|
|
||||||
data={formatCustomFormatScore(customFormatScore)}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'movieFileRenamed') {
|
|
||||||
const {
|
|
||||||
sourcePath,
|
|
||||||
sourceRelativePath,
|
|
||||||
path,
|
|
||||||
relativePath
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourcePath')}
|
|
||||||
data={sourcePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('SourceRelativePath')}
|
|
||||||
data={sourceRelativePath}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationPath')}
|
|
||||||
data={path}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('DestinationRelativePath')}
|
|
||||||
data={relativePath}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventType === 'downloadIgnored') {
|
|
||||||
const {
|
|
||||||
message
|
|
||||||
} = data;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
downloadId ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('GrabId')}
|
|
||||||
data={downloadId}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
message ?
|
|
||||||
<DescriptionListItem
|
|
||||||
title={translate('Message')}
|
|
||||||
data={message}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DescriptionList>
|
|
||||||
<DescriptionListItem
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title={translate('Name')}
|
|
||||||
data={sourceTitle}
|
|
||||||
/>
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryDetails.propTypes = {
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryDetails;
|
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||||
|
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import {
|
||||||
|
DownloadFailedHistory,
|
||||||
|
DownloadFolderImportedHistory,
|
||||||
|
DownloadIgnoredHistory,
|
||||||
|
GrabbedHistoryData,
|
||||||
|
HistoryData,
|
||||||
|
HistoryEventType,
|
||||||
|
MovieFileDeletedHistory,
|
||||||
|
MovieFileRenamedHistory,
|
||||||
|
} from 'typings/History';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
|
interface HistoryDetailsProps {
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetails(props: HistoryDetailsProps) {
|
||||||
|
const { eventType, sourceTitle, data, downloadId } = props;
|
||||||
|
|
||||||
|
const { shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventType === 'grabbed') {
|
||||||
|
const {
|
||||||
|
indexer,
|
||||||
|
releaseGroup,
|
||||||
|
movieMatchType,
|
||||||
|
releaseSource,
|
||||||
|
customFormatScore,
|
||||||
|
nzbInfoUrl,
|
||||||
|
downloadClient,
|
||||||
|
downloadClientName,
|
||||||
|
age,
|
||||||
|
ageHours,
|
||||||
|
ageMinutes,
|
||||||
|
publishedDate,
|
||||||
|
size,
|
||||||
|
} = data as GrabbedHistoryData;
|
||||||
|
|
||||||
|
const downloadClientNameInfo = downloadClientName ?? downloadClient;
|
||||||
|
|
||||||
|
let releaseSourceMessage = '';
|
||||||
|
|
||||||
|
switch (releaseSource) {
|
||||||
|
case 'Unknown':
|
||||||
|
releaseSourceMessage = translate('Unknown');
|
||||||
|
break;
|
||||||
|
case 'Rss':
|
||||||
|
releaseSourceMessage = translate('Rss');
|
||||||
|
break;
|
||||||
|
case 'Search':
|
||||||
|
releaseSourceMessage = translate('Search');
|
||||||
|
break;
|
||||||
|
case 'UserInvokedSearch':
|
||||||
|
releaseSourceMessage = translate('UserInvokedSearch');
|
||||||
|
break;
|
||||||
|
case 'InteractiveSearch':
|
||||||
|
releaseSourceMessage = translate('InteractiveSearch');
|
||||||
|
break;
|
||||||
|
case 'ReleasePush':
|
||||||
|
releaseSourceMessage = translate('ReleasePush');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
releaseSourceMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{indexer ? (
|
||||||
|
<DescriptionListItem title={translate('Indexer')} data={indexer} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseGroup ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseGroup')}
|
||||||
|
data={releaseGroup}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{movieMatchType ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('MovieMatchType')}
|
||||||
|
data={movieMatchType}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{releaseSource ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ReleaseSource')}
|
||||||
|
data={releaseSourceMessage}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{nzbInfoUrl ? (
|
||||||
|
<span>
|
||||||
|
<DescriptionListItemTitle>
|
||||||
|
{translate('InfoUrl')}
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription>
|
||||||
|
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadClientNameInfo ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DownloadClient')}
|
||||||
|
data={downloadClientNameInfo}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{age || ageHours || ageMinutes ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('AgeWhenGrabbed')}
|
||||||
|
data={formatAge(age, ageHours, ageMinutes)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{publishedDate ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('PublishedDate')}
|
||||||
|
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, {
|
||||||
|
includeSeconds: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{size ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('Size')}
|
||||||
|
data={formatBytes(size)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFailed') {
|
||||||
|
const { message, indexer } = data as DownloadFailedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{indexer ? (
|
||||||
|
<DescriptionListItem title={translate('Indexer')} data={indexer} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadFolderImported') {
|
||||||
|
const { customFormatScore, droppedPath, importedPath, size } =
|
||||||
|
data as DownloadFolderImportedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{droppedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Source')}
|
||||||
|
data={droppedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{importedPath ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('ImportedTo')}
|
||||||
|
data={importedPath}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{size ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('FileSize')}
|
||||||
|
data={formatBytes(size)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'movieFileDeleted') {
|
||||||
|
const { reason, customFormatScore, size } = data as MovieFileDeletedHistory;
|
||||||
|
|
||||||
|
let reasonMessage = '';
|
||||||
|
|
||||||
|
switch (reason) {
|
||||||
|
case 'Manual':
|
||||||
|
reasonMessage = translate('DeletedReasonManual');
|
||||||
|
break;
|
||||||
|
case 'MissingFromDisk':
|
||||||
|
reasonMessage = translate('DeletedReasonMovieMissingFromDisk');
|
||||||
|
break;
|
||||||
|
case 'Upgrade':
|
||||||
|
reasonMessage = translate('DeletedReasonUpgrade');
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reasonMessage = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem title={translate('Name')} data={sourceTitle} />
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('Reason')} data={reasonMessage} />
|
||||||
|
|
||||||
|
{customFormatScore && customFormatScore !== '0' ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(parseInt(customFormatScore))}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{size ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('FileSize')}
|
||||||
|
data={formatBytes(size)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'movieFileRenamed') {
|
||||||
|
const { sourcePath, sourceRelativePath, path, relativePath } =
|
||||||
|
data as MovieFileRenamedHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourcePath')}
|
||||||
|
data={sourcePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('SourceRelativePath')}
|
||||||
|
data={sourceRelativePath}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem title={translate('DestinationPath')} data={path} />
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('DestinationRelativePath')}
|
||||||
|
data={relativePath}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'downloadIgnored') {
|
||||||
|
const { message } = data as DownloadIgnoredHistory;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadId ? (
|
||||||
|
<DescriptionListItem title={translate('GrabId')} data={downloadId} />
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{message ? (
|
||||||
|
<DescriptionListItem title={translate('Message')} data={message} />
|
||||||
|
) : null}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title={translate('Name')}
|
||||||
|
data={sourceTitle}
|
||||||
|
/>
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryDetails;
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import HistoryDetails from './HistoryDetails';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(uiSettings) => {
|
|
||||||
return {
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(HistoryDetails);
|
|
||||||
+29
-49
@@ -1,4 +1,3 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||||
@@ -8,11 +7,12 @@ import ModalContent from 'Components/Modal/ModalContent';
|
|||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import HistoryDetails from './HistoryDetails';
|
import HistoryDetails from './HistoryDetails';
|
||||||
import styles from './HistoryDetailsModal.css';
|
import styles from './HistoryDetailsModal.css';
|
||||||
|
|
||||||
function getHeaderTitle(eventType) {
|
function getHeaderTitle(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('Grabbed');
|
return translate('Grabbed');
|
||||||
@@ -31,29 +31,33 @@ function getHeaderTitle(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryDetailsModal(props) {
|
interface HistoryDetailsModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
isMarkingAsFailed?: boolean;
|
||||||
|
onMarkAsFailedPress: () => void;
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryDetailsModal(props: HistoryDetailsModalProps) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
eventType,
|
eventType,
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
data,
|
data,
|
||||||
downloadId,
|
downloadId,
|
||||||
isMarkingAsFailed,
|
isMarkingAsFailed = false,
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onMarkAsFailedPress,
|
onMarkAsFailedPress,
|
||||||
onModalClose
|
onModalClose,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>{getHeaderTitle(eventType)}</ModalHeader>
|
||||||
{getHeaderTitle(eventType)}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<HistoryDetails
|
<HistoryDetails
|
||||||
@@ -61,50 +65,26 @@ function HistoryDetailsModal(props) {
|
|||||||
sourceTitle={sourceTitle}
|
sourceTitle={sourceTitle}
|
||||||
data={data}
|
data={data}
|
||||||
downloadId={downloadId}
|
downloadId={downloadId}
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
/>
|
/>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{
|
{eventType === 'grabbed' && (
|
||||||
eventType === 'grabbed' &&
|
<SpinnerButton
|
||||||
<SpinnerButton
|
className={styles.markAsFailedButton}
|
||||||
className={styles.markAsFailedButton}
|
kind={kinds.DANGER}
|
||||||
kind={kinds.DANGER}
|
isSpinning={isMarkingAsFailed}
|
||||||
isSpinning={isMarkingAsFailed}
|
onPress={onMarkAsFailedPress}
|
||||||
onPress={onMarkAsFailedPress}
|
>
|
||||||
>
|
{translate('MarkAsFailed')}
|
||||||
{translate('MarkAsFailed')}
|
</SpinnerButton>
|
||||||
</SpinnerButton>
|
)}
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
<Button onPress={onModalClose}>{translate('Close')}</Button>
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Close')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryDetailsModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryDetailsModal.defaultProps = {
|
|
||||||
isMarkingAsFailed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryDetailsModal;
|
export default HistoryDetailsModal;
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
|
||||||
import TablePager from 'Components/Table/TablePager';
|
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import HistoryFilterModal from './HistoryFilterModal';
|
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
|
||||||
|
|
||||||
class History extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
isMoviesFetching,
|
|
||||||
isMoviesPopulated,
|
|
||||||
moviesError,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
totalRecords,
|
|
||||||
onFilterSelect,
|
|
||||||
onFirstPagePress,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isFetchingAny = isFetching || isMoviesFetching;
|
|
||||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
|
|
||||||
const hasError = error || moviesError;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('History')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Refresh')}
|
|
||||||
iconName={icons.REFRESH}
|
|
||||||
isSpinning={isFetching}
|
|
||||||
onPress={onFirstPagePress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<TableOptionsModalWrapper
|
|
||||||
{...otherProps}
|
|
||||||
columns={columns}
|
|
||||||
>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.TABLE}
|
|
||||||
/>
|
|
||||||
</TableOptionsModalWrapper>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={HistoryFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody>
|
|
||||||
{
|
|
||||||
isFetchingAny && !isAllPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetchingAny && hasError &&
|
|
||||||
<Alert kind={kinds.DANGER}>
|
|
||||||
{translate('HistoryLoadError')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// If history isPopulated and it's empty show no history found and don't
|
|
||||||
// wait for the episodes to populate because they are never coming.
|
|
||||||
|
|
||||||
isPopulated && !hasError && !items.length &&
|
|
||||||
<Alert kind={kinds.INFO}>
|
|
||||||
{translate('NoHistoryFound')}
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isAllPopulated && !hasError && !!items.length &&
|
|
||||||
<div>
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<HistoryRowConnector
|
|
||||||
key={item.id}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
|
|
||||||
<TablePager
|
|
||||||
totalRecords={totalRecords}
|
|
||||||
isFetching={isFetching}
|
|
||||||
onFirstPagePress={onFirstPagePress}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
History.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
isMoviesFetching: PropTypes.bool.isRequired,
|
|
||||||
isMoviesPopulated: PropTypes.bool.isRequired,
|
|
||||||
moviesError: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
totalRecords: PropTypes.number,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
|
||||||
onFirstPagePress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default History;
|
|
||||||
@@ -0,0 +1,216 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import TablePager from 'Components/Table/TablePager';
|
||||||
|
import usePaging from 'Components/Table/usePaging';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
|
import createMoviesFetchingSelector from 'Movie/createMoviesFetchingSelector';
|
||||||
|
import {
|
||||||
|
clearHistory,
|
||||||
|
fetchHistory,
|
||||||
|
gotoHistoryPage,
|
||||||
|
setHistoryFilter,
|
||||||
|
setHistorySort,
|
||||||
|
setHistoryTableOption,
|
||||||
|
} from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import { TableOptionsChangePayload } from 'typings/Table';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
|
import HistoryRow from './HistoryRow';
|
||||||
|
|
||||||
|
function History() {
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
totalRecords,
|
||||||
|
} = useSelector((state: AppState) => state.history);
|
||||||
|
|
||||||
|
const { isMoviesFetching, isMoviesPopulated, moviesError } = useSelector(
|
||||||
|
createMoviesFetchingSelector()
|
||||||
|
);
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('history'));
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const isFetchingAny = isFetching || isMoviesFetching;
|
||||||
|
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length);
|
||||||
|
const hasError = error || moviesError;
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleFirstPagePress,
|
||||||
|
handlePreviousPagePress,
|
||||||
|
handleNextPagePress,
|
||||||
|
handleLastPagePress,
|
||||||
|
handlePageSelect,
|
||||||
|
} = usePaging({
|
||||||
|
page,
|
||||||
|
totalPages,
|
||||||
|
gotoPage: gotoHistoryPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(selectedFilterKey: string | number) => {
|
||||||
|
dispatch(setHistoryFilter({ selectedFilterKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSortPress = useCallback(
|
||||||
|
(sortKey: string) => {
|
||||||
|
dispatch(setHistorySort({ sortKey }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableOptionChange = useCallback(
|
||||||
|
(payload: TableOptionsChangePayload) => {
|
||||||
|
dispatch(setHistoryTableOption(payload));
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
dispatch(gotoHistoryPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoHistoryPage({ page: 1 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearHistory());
|
||||||
|
};
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('History')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Refresh')}
|
||||||
|
iconName={icons.REFRESH}
|
||||||
|
isSpinning={isFetching}
|
||||||
|
onPress={handleFirstPagePress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody>
|
||||||
|
{isFetchingAny && !isAllPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetchingAny && hasError ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('HistoryLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{
|
||||||
|
// If history isPopulated and it's empty show no history found and don't
|
||||||
|
// wait for the movies to populate because they are never coming.
|
||||||
|
|
||||||
|
isPopulated && !hasError && !items.length ? (
|
||||||
|
<Alert kind={kinds.INFO}>{translate('NoHistoryFound')}</Alert>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
||||||
|
{isAllPopulated && !hasError && items.length ? (
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
pageSize={pageSize}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={handleTableOptionChange}
|
||||||
|
onSortPress={handleSortPress}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{items.map((item) => {
|
||||||
|
return (
|
||||||
|
<HistoryRow key={item.id} columns={columns} {...item} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<TablePager
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
totalRecords={totalRecords}
|
||||||
|
isFetching={isFetching}
|
||||||
|
onFirstPagePress={handleFirstPagePress}
|
||||||
|
onPreviousPagePress={handlePreviousPagePress}
|
||||||
|
onNextPagePress={handleNextPagePress}
|
||||||
|
onLastPagePress={handleLastPagePress}
|
||||||
|
onPageSelect={handlePageSelect}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</PageContentBody>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default History;
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import History from './History';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.history,
|
|
||||||
(state) => state.movies,
|
|
||||||
createCustomFiltersSelector('history'),
|
|
||||||
(history, movies, customFilters) => {
|
|
||||||
return {
|
|
||||||
isMoviesFetching: movies.isFetching,
|
|
||||||
isMoviesPopulated: movies.isPopulated,
|
|
||||||
moviesError: movies.error,
|
|
||||||
customFilters,
|
|
||||||
...history
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...historyActions
|
|
||||||
};
|
|
||||||
|
|
||||||
class HistoryConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchHistory,
|
|
||||||
gotoHistoryFirstPage
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchHistory();
|
|
||||||
} else {
|
|
||||||
gotoHistoryFirstPage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
this.props.fetchHistory();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onFirstPagePress = () => {
|
|
||||||
this.props.gotoHistoryFirstPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPagePress = () => {
|
|
||||||
this.props.gotoHistoryPreviousPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPagePress = () => {
|
|
||||||
this.props.gotoHistoryNextPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onLastPagePress = () => {
|
|
||||||
this.props.gotoHistoryLastPage();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPageSelect = (page) => {
|
|
||||||
this.props.gotoHistoryPage({ page });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSortPress = (sortKey) => {
|
|
||||||
this.props.setHistorySort({ sortKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setHistoryFilter({ selectedFilterKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setHistoryTableOption(payload);
|
|
||||||
|
|
||||||
if (payload.pageSize) {
|
|
||||||
this.props.gotoHistoryFirstPage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<History
|
|
||||||
onFirstPagePress={this.onFirstPagePress}
|
|
||||||
onPreviousPagePress={this.onPreviousPagePress}
|
|
||||||
onNextPagePress={this.onNextPagePress}
|
|
||||||
onLastPagePress={this.onLastPagePress}
|
|
||||||
onPageSelect={this.onPageSelect}
|
|
||||||
onSortPress={this.onSortPress}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryConnector.propTypes = {
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
fetchHistory: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryPreviousPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryNextPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryLastPage: PropTypes.func.isRequired,
|
|
||||||
gotoHistoryPage: PropTypes.func.isRequired,
|
|
||||||
setHistorySort: PropTypes.func.isRequired,
|
|
||||||
setHistoryFilter: PropTypes.func.isRequired,
|
|
||||||
setHistoryTableOption: PropTypes.func.isRequired,
|
|
||||||
clearHistory: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
|
|
||||||
);
|
|
||||||
+29
-27
@@ -1,12 +1,17 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import {
|
||||||
|
GrabbedHistoryData,
|
||||||
|
HistoryData,
|
||||||
|
HistoryEventType,
|
||||||
|
MovieFileDeletedHistory,
|
||||||
|
} from 'typings/History';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType, data) {
|
function getIconName(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
@@ -17,7 +22,9 @@ function getIconName(eventType, data) {
|
|||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return icons.DOWNLOADING;
|
return icons.DOWNLOADING;
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? icons.FILE_MISSING
|
||||||
|
: icons.DELETE;
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return icons.ORGANIZE;
|
return icons.ORGANIZE;
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
@@ -27,7 +34,7 @@ function getIconName(eventType, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIconKind(eventType) {
|
function getIconKind(eventType: HistoryEventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return kinds.DANGER;
|
return kinds.DANGER;
|
||||||
@@ -36,52 +43,47 @@ function getIconKind(eventType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTooltip(eventType, data) {
|
function getTooltip(eventType: HistoryEventType, data: HistoryData) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
|
return translate('MovieGrabbedTooltip', {
|
||||||
|
indexer: (data as GrabbedHistoryData).indexer,
|
||||||
|
downloadClient: (data as GrabbedHistoryData).downloadClient,
|
||||||
|
});
|
||||||
case 'movieFolderImported':
|
case 'movieFolderImported':
|
||||||
return translate('MovieFolderImportedTooltip');
|
return translate('MovieFolderImportedTooltip');
|
||||||
case 'downloadFolderImported':
|
case 'downloadFolderImported':
|
||||||
return translate('MovieImportedTooltip');
|
return translate('MovieImportedTooltip');
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return translate('MovieDownloadFailedTooltip');
|
return translate('DownloadFailedMovieTooltip');
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
|
return (data as MovieFileDeletedHistory).reason === 'MissingFromDisk'
|
||||||
|
? translate('MovieFileMissingTooltip')
|
||||||
|
: translate('MovieFileDeletedTooltip');
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return translate('MovieFileRenamedTooltip');
|
return translate('MovieFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
return translate('MovieDownloadIgnoredTooltip');
|
return translate('DownloadIgnoredMovieTooltip');
|
||||||
default:
|
default:
|
||||||
return translate('UnknownEventTooltip');
|
return translate('UnknownEventTooltip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function HistoryEventTypeCell({ eventType, data }) {
|
interface HistoryEventTypeCellProps {
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
data: HistoryData;
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryEventTypeCell({ eventType, data }: HistoryEventTypeCellProps) {
|
||||||
const iconName = getIconName(eventType, data);
|
const iconName = getIconName(eventType, data);
|
||||||
const iconKind = getIconKind(eventType);
|
const iconKind = getIconKind(eventType);
|
||||||
const tooltip = getTooltip(eventType, data);
|
const tooltip = getTooltip(eventType, data);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.cell} title={tooltip}>
|
||||||
className={styles.cell}
|
<Icon name={iconName} kind={iconKind} />
|
||||||
title={tooltip}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
name={iconName}
|
|
||||||
kind={iconKind}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryEventTypeCell.propTypes = {
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryEventTypeCell.defaultProps = {
|
|
||||||
data: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryEventTypeCell;
|
export default HistoryEventTypeCell;
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import MovieFormats from 'Movie/MovieFormats';
|
|
||||||
import MovieLanguages from 'Movie/MovieLanguages';
|
|
||||||
import MovieQuality from 'Movie/MovieQuality';
|
|
||||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
|
||||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
|
||||||
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
|
||||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
|
||||||
import styles from './HistoryRow.css';
|
|
||||||
|
|
||||||
class HistoryRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
prevProps.isMarkingAsFailed &&
|
|
||||||
!this.props.isMarkingAsFailed &&
|
|
||||||
!this.props.markAsFailedError
|
|
||||||
) {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onDetailsPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
movie,
|
|
||||||
quality,
|
|
||||||
customFormats,
|
|
||||||
customFormatScore,
|
|
||||||
languages,
|
|
||||||
qualityCutoffNotMet,
|
|
||||||
eventType,
|
|
||||||
sourceTitle,
|
|
||||||
date,
|
|
||||||
data,
|
|
||||||
downloadId,
|
|
||||||
isMarkingAsFailed,
|
|
||||||
columns,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
onMarkAsFailedPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!movie) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'eventType') {
|
|
||||||
return (
|
|
||||||
<HistoryEventTypeCell
|
|
||||||
key={name}
|
|
||||||
eventType={eventType}
|
|
||||||
data={data}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'movieMetadata.sortTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieTitleLink
|
|
||||||
titleSlug={movie.titleSlug}
|
|
||||||
title={movie.title}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'languages') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieLanguages
|
|
||||||
languages={languages}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'quality') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieQuality
|
|
||||||
quality={quality}
|
|
||||||
isCutoffMet={qualityCutoffNotMet}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormats') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<MovieFormats
|
|
||||||
formats={customFormats}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'date') {
|
|
||||||
return (
|
|
||||||
<RelativeDateCell
|
|
||||||
key={name}
|
|
||||||
date={date}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'downloadClient') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.downloadClient}
|
|
||||||
>
|
|
||||||
{data.downloadClient}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'indexer') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.indexer}
|
|
||||||
>
|
|
||||||
{data.indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'customFormatScore') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.customFormatScore}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
anchor={formatCustomFormatScore(
|
|
||||||
customFormatScore,
|
|
||||||
customFormats.length
|
|
||||||
)}
|
|
||||||
tooltip={<MovieFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'releaseGroup') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.releaseGroup}
|
|
||||||
>
|
|
||||||
{data.releaseGroup}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'sourceTitle') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
>
|
|
||||||
{sourceTitle}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'details') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.details}
|
|
||||||
>
|
|
||||||
<div className={styles.actionContents}>
|
|
||||||
<IconButton
|
|
||||||
name={icons.INFO}
|
|
||||||
onPress={this.onDetailsPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
<HistoryDetailsModal
|
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
|
||||||
eventType={eventType}
|
|
||||||
sourceTitle={sourceTitle}
|
|
||||||
data={data}
|
|
||||||
downloadId={downloadId}
|
|
||||||
isMarkingAsFailed={isMarkingAsFailed}
|
|
||||||
shortDateFormat={shortDateFormat}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
|
||||||
onModalClose={this.onDetailsModalClose}
|
|
||||||
/>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryRow.propTypes = {
|
|
||||||
movieId: PropTypes.number,
|
|
||||||
movie: PropTypes.object.isRequired,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
quality: PropTypes.object.isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
|
||||||
eventType: PropTypes.string.isRequired,
|
|
||||||
sourceTitle: PropTypes.string.isRequired,
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
data: PropTypes.object.isRequired,
|
|
||||||
downloadId: PropTypes.string,
|
|
||||||
isMarkingAsFailed: PropTypes.bool,
|
|
||||||
markAsFailedError: PropTypes.object,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
HistoryRow.defaultProps = {
|
|
||||||
customFormats: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default HistoryRow;
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
|
||||||
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Column from 'Components/Table/Column';
|
||||||
|
import TableRow from 'Components/Table/TableRow';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import MovieFormats from 'Movie/MovieFormats';
|
||||||
|
import MovieLanguages from 'Movie/MovieLanguages';
|
||||||
|
import MovieQuality from 'Movie/MovieQuality';
|
||||||
|
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||||
|
import useMovie from 'Movie/useMovie';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||||
|
import CustomFormat from 'typings/CustomFormat';
|
||||||
|
import { HistoryData, HistoryEventType } from 'typings/History';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
|
import HistoryDetailsModal from './Details/HistoryDetailsModal';
|
||||||
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
|
import styles from './HistoryRow.css';
|
||||||
|
|
||||||
|
interface HistoryRowProps {
|
||||||
|
id: number;
|
||||||
|
movieId: number;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats?: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
qualityCutoffNotMet: boolean;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
sourceTitle: string;
|
||||||
|
date: string;
|
||||||
|
data: HistoryData;
|
||||||
|
downloadId?: string;
|
||||||
|
isMarkingAsFailed?: boolean;
|
||||||
|
markAsFailedError?: object;
|
||||||
|
columns: Column[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function HistoryRow(props: HistoryRowProps) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
movieId,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
customFormats = [],
|
||||||
|
customFormatScore,
|
||||||
|
qualityCutoffNotMet,
|
||||||
|
eventType,
|
||||||
|
sourceTitle,
|
||||||
|
date,
|
||||||
|
data,
|
||||||
|
downloadId,
|
||||||
|
isMarkingAsFailed = false,
|
||||||
|
markAsFailedError,
|
||||||
|
columns,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const wasMarkingAsFailed = usePrevious(isMarkingAsFailed);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const movie = useMovie(movieId);
|
||||||
|
|
||||||
|
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleDetailsPress = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(true);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleDetailsModalClose = useCallback(() => {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
}, [setIsDetailsModalOpen]);
|
||||||
|
|
||||||
|
const handleMarkAsFailedPress = useCallback(() => {
|
||||||
|
dispatch(markAsFailed({ id }));
|
||||||
|
}, [id, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasMarkingAsFailed && !isMarkingAsFailed && !markAsFailedError) {
|
||||||
|
setIsDetailsModalOpen(false);
|
||||||
|
dispatch(fetchHistory());
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
wasMarkingAsFailed,
|
||||||
|
isMarkingAsFailed,
|
||||||
|
markAsFailedError,
|
||||||
|
setIsDetailsModalOpen,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!movie) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow>
|
||||||
|
{columns.map((column) => {
|
||||||
|
const { name, isVisible } = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'eventType') {
|
||||||
|
return (
|
||||||
|
<HistoryEventTypeCell
|
||||||
|
key={name}
|
||||||
|
eventType={eventType}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieTitleLink titleSlug={movie.titleSlug} title={movie.title} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'languages') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieQuality
|
||||||
|
quality={quality}
|
||||||
|
isCutoffNotMet={qualityCutoffNotMet}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormats') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
<MovieFormats formats={customFormats} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'date') {
|
||||||
|
return <RelativeDateCell key={name} date={date} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'downloadClient') {
|
||||||
|
const downloadClientName =
|
||||||
|
'downloadClientName' in data ? data.downloadClientName : null;
|
||||||
|
const downloadClient =
|
||||||
|
'downloadClient' in data ? data.downloadClient : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.downloadClient}>
|
||||||
|
{downloadClientName ?? downloadClient ?? ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'indexer') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.indexer}>
|
||||||
|
{'indexer' in data ? data.indexer : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'customFormatScore') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatCustomFormatScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<MovieFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'releaseGroup') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.releaseGroup}>
|
||||||
|
{'releaseGroup' in data ? data.releaseGroup : ''}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'sourceTitle') {
|
||||||
|
return <TableRowCell key={name}>{sourceTitle}</TableRowCell>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'details') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name} className={styles.details}>
|
||||||
|
<IconButton name={icons.INFO} onPress={handleDetailsPress} />
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
|
|
||||||
|
<HistoryDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
eventType={eventType}
|
||||||
|
sourceTitle={sourceTitle}
|
||||||
|
data={data}
|
||||||
|
downloadId={downloadId}
|
||||||
|
isMarkingAsFailed={isMarkingAsFailed}
|
||||||
|
onMarkAsFailedPress={handleMarkAsFailedPress}
|
||||||
|
onModalClose={handleDetailsModalClose}
|
||||||
|
/>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryRow;
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import HistoryRow from './HistoryRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createMovieSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(movie, uiSettings) => {
|
|
||||||
return {
|
|
||||||
movie,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
fetchHistory,
|
|
||||||
markAsFailed
|
|
||||||
};
|
|
||||||
|
|
||||||
class HistoryRowConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (
|
|
||||||
prevProps.isMarkingAsFailed &&
|
|
||||||
!this.props.isMarkingAsFailed &&
|
|
||||||
!this.props.markAsFailedError
|
|
||||||
) {
|
|
||||||
this.props.fetchHistory();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMarkAsFailedPress = () => {
|
|
||||||
this.props.markAsFailed({ id: this.props.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<HistoryRow
|
|
||||||
{...this.props}
|
|
||||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
HistoryRowConnector.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
isMarkingAsFailed: PropTypes.bool,
|
|
||||||
markAsFailedError: PropTypes.object,
|
|
||||||
fetchHistory: PropTypes.func.isRequired,
|
|
||||||
markAsFailed: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryRowConnector);
|
|
||||||
@@ -183,7 +183,7 @@ function Queue() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleFilterSelect = useCallback(
|
const handleFilterSelect = useCallback(
|
||||||
(selectedFilterKey: string) => {
|
(selectedFilterKey: string | number) => {
|
||||||
dispatch(setQueueFilter({ selectedFilterKey }));
|
dispatch(setQueueFilter({ selectedFilterKey }));
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
|||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
import { gotoQueuePage, setQueueOption } from 'Store/Actions/queueActions';
|
||||||
import { CheckInputChanged } from 'typings/inputs';
|
import { InputChanged } from 'typings/inputs';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
function QueueOptions() {
|
function QueueOptions() {
|
||||||
@@ -16,7 +16,7 @@ function QueueOptions() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleOptionChange = useCallback(
|
const handleOptionChange = useCallback(
|
||||||
({ name, value }: CheckInputChanged) => {
|
({ name, value }: InputChanged<boolean>) => {
|
||||||
dispatch(
|
dispatch(
|
||||||
setQueueOption({
|
setQueueOption({
|
||||||
[name]: value,
|
[name]: value,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon, { IconProps } from 'Components/Icon';
|
import Icon, { IconKind } from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import TooltipPosition from 'Helpers/Props/TooltipPosition';
|
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
|
||||||
import {
|
import {
|
||||||
QueueTrackedDownloadState,
|
QueueTrackedDownloadState,
|
||||||
QueueTrackedDownloadStatus,
|
QueueTrackedDownloadStatus,
|
||||||
@@ -61,7 +61,7 @@ function QueueStatus(props: QueueStatusProps) {
|
|||||||
|
|
||||||
// status === 'downloading'
|
// status === 'downloading'
|
||||||
let iconName = icons.DOWNLOADING;
|
let iconName = icons.DOWNLOADING;
|
||||||
let iconKind: IconProps['kind'] = kinds.DEFAULT;
|
let iconKind: IconKind = kinds.DEFAULT;
|
||||||
let title = translate('Downloading');
|
let title = translate('Downloading');
|
||||||
|
|
||||||
if (status === 'paused') {
|
if (status === 'paused') {
|
||||||
|
|||||||
@@ -82,8 +82,7 @@ class AddNewMovie extends Component {
|
|||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
hasExistingMovies,
|
hasExistingMovies
|
||||||
colorImpairedMode
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const term = this.state.term;
|
const term = this.state.term;
|
||||||
@@ -150,7 +149,6 @@ class AddNewMovie extends Component {
|
|||||||
return (
|
return (
|
||||||
<AddNewMovieSearchResultConnector
|
<AddNewMovieSearchResultConnector
|
||||||
key={item.tmdbId}
|
key={item.tmdbId}
|
||||||
colorImpairedMode={colorImpairedMode}
|
|
||||||
{...item}
|
{...item}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -223,8 +221,7 @@ AddNewMovie.propTypes = {
|
|||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
hasExistingMovies: PropTypes.bool.isRequired,
|
hasExistingMovies: PropTypes.bool.isRequired,
|
||||||
onMovieLookupChange: PropTypes.func.isRequired,
|
onMovieLookupChange: PropTypes.func.isRequired,
|
||||||
onClearMovieLookup: PropTypes.func.isRequired,
|
onClearMovieLookup: PropTypes.func.isRequired
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AddNewMovie;
|
export default AddNewMovie;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
|||||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
import parseUrl from 'Utilities/String/parseUrl';
|
import parseUrl from 'Utilities/String/parseUrl';
|
||||||
@@ -17,15 +16,13 @@ function createMapStateToProps() {
|
|||||||
(state) => state.addMovie,
|
(state) => state.addMovie,
|
||||||
(state) => state.movies.items.length,
|
(state) => state.movies.items.length,
|
||||||
(state) => state.router.location,
|
(state) => state.router.location,
|
||||||
createUISettingsSelector(),
|
(addMovie, existingMoviesCount, location) => {
|
||||||
(addMovie, existingMoviesCount, location, uiSettings) => {
|
|
||||||
const { params } = parseUrl(location.search);
|
const { params } = parseUrl(location.search);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...addMovie,
|
...addMovie,
|
||||||
term: params.term,
|
term: params.term,
|
||||||
hasExistingMovies: existingMoviesCount > 0,
|
hasExistingMovies: existingMoviesCount > 0
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ class AddNewMovieModalContent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.overview}>
|
{overview ? (
|
||||||
{overview}
|
<div className={styles.overview}>{overview}</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
<Form>
|
<Form>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
@@ -98,7 +98,9 @@ class AddNewMovieModalContent extends Component {
|
|||||||
movieFolder: folder,
|
movieFolder: folder,
|
||||||
isWindows
|
isWindows
|
||||||
}}
|
}}
|
||||||
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
|
helpText={translate('AddNewMovieRootFolderHelpText', {
|
||||||
|
folder
|
||||||
|
})}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...rootFolderPath}
|
{...rootFolderPath}
|
||||||
/>
|
/>
|
||||||
@@ -110,7 +112,7 @@ class AddNewMovieModalContent extends Component {
|
|||||||
</FormLabel>
|
</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...monitor}
|
{...monitor}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -73,12 +74,9 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isExcluded,
|
isExcluded,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
colorImpairedMode,
|
|
||||||
id,
|
|
||||||
monitored,
|
monitored,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
movieFile,
|
movieFile,
|
||||||
queueItem,
|
|
||||||
runtime,
|
runtime,
|
||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
certification
|
certification
|
||||||
@@ -249,9 +247,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,20 +276,18 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
}
|
}
|
||||||
canFlip={true}
|
canFlip={true}
|
||||||
kind={kinds.INVERSE}
|
kind={kinds.INVERSE}
|
||||||
position={tooltipPositions.BOTTOM}
|
position={tooltipPositions.TOP}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
isExistingMovie && isSmallScreen &&
|
isExistingMovie && isSmallScreen &&
|
||||||
<MovieStatusLabel
|
<MovieStatusLabel
|
||||||
status={status}
|
movieId={existingMovieId}
|
||||||
hasMovieFiles={hasMovieFile}
|
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
queueItem={queueItem}
|
hasMovieFiles={hasMovieFile}
|
||||||
id={id}
|
status={status}
|
||||||
useLabel={true}
|
useLabel={true}
|
||||||
colorImpairedMode={colorImpairedMode}
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -338,12 +332,9 @@ AddNewMovieSearchResult.propTypes = {
|
|||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
isExcluded: PropTypes.bool,
|
isExcluded: PropTypes.bool,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
id: PropTypes.number,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
movieFile: PropTypes.object,
|
movieFile: PropTypes.object,
|
||||||
queueItem: PropTypes.object,
|
|
||||||
colorImpairedMode: PropTypes.bool,
|
|
||||||
runtime: PropTypes.number.isRequired,
|
runtime: PropTypes.number.isRequired,
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||||
certification: PropTypes.string
|
certification: PropTypes.string
|
||||||
|
|||||||
@@ -8,19 +8,16 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
createExistingMovieSelector(),
|
createExistingMovieSelector(),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(state) => state.queue.details.items,
|
|
||||||
(state) => state.movieFiles.items,
|
(state) => state.movieFiles.items,
|
||||||
(state, { internalId }) => internalId,
|
(state, { internalId }) => internalId,
|
||||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||||
(isExistingMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
(isExistingMovie, dimensions, movieFiles, internalId, movieRuntimeFormat) => {
|
||||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
|
||||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
existingMovieId: internalId,
|
existingMovieId: internalId,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
queueItem,
|
|
||||||
movieFile,
|
movieFile,
|
||||||
movieRuntimeFormat
|
movieRuntimeFormat
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,10 @@
|
|||||||
.inputContainer {
|
.inputContainer {
|
||||||
margin-right: 20px;
|
margin-right: 20px;
|
||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
|
|
||||||
div {
|
|
||||||
margin-top: 10px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
margin-bottom: 3px;
|
margin-bottom: 10px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class ImportMovieFooter extends Component {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
isDisabled={!selectedCount}
|
isDisabled={!selectedCount}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ function ImportMovieRow(props) {
|
|||||||
|
|
||||||
<VirtualTableRowCell className={styles.monitor}>
|
<VirtualTableRowCell className={styles.monitor}>
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.MOVIE_MONITORED_SELECT}
|
type={inputTypes.MONITOR_MOVIES_SELECT}
|
||||||
name="monitor"
|
name="monitor"
|
||||||
value={monitor}
|
value={monitor}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
@@ -81,7 +81,6 @@ ImportMovieRow.propTypes = {
|
|||||||
selectedMovie: PropTypes.object,
|
selectedMovie: PropTypes.object,
|
||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
queued: PropTypes.bool.isRequired,
|
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
onSelectedChange: PropTypes.func.isRequired,
|
onSelectedChange: PropTypes.func.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired
|
onInputChange: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ImportMovieSelectMovie extends Component {
|
|||||||
id={this._buttonId}
|
id={this._buttonId}
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
ref={ref}
|
// ref={ref}
|
||||||
className={styles.button}
|
className={styles.button}
|
||||||
component="div"
|
component="div"
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
@@ -255,7 +255,7 @@ class ImportMovieSelectMovie extends Component {
|
|||||||
items.map((item) => {
|
items.map((item) => {
|
||||||
return (
|
return (
|
||||||
<ImportMovieSearchResultConnector
|
<ImportMovieSearchResultConnector
|
||||||
key={item.tvdbId}
|
key={item.tmdbId}
|
||||||
tmdbId={item.tmdbId}
|
tmdbId={item.tmdbId}
|
||||||
title={item.title}
|
title={item.title}
|
||||||
year={item.year}
|
year={item.year}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { ConnectedRouter } from 'connected-react-router';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import DocumentTitle from 'react-document-title';
|
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
import PageConnector from 'Components/Page/PageConnector';
|
|
||||||
import ApplyTheme from './ApplyTheme';
|
|
||||||
import AppRoutes from './AppRoutes';
|
|
||||||
|
|
||||||
function App({ store, history }) {
|
|
||||||
return (
|
|
||||||
<DocumentTitle title={window.Radarr.instanceName}>
|
|
||||||
<Provider store={store}>
|
|
||||||
<ConnectedRouter history={history}>
|
|
||||||
<ApplyTheme />
|
|
||||||
<PageConnector>
|
|
||||||
<AppRoutes app={App} />
|
|
||||||
</PageConnector>
|
|
||||||
</ConnectedRouter>
|
|
||||||
</Provider>
|
|
||||||
</DocumentTitle>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
App.propTypes = {
|
|
||||||
store: PropTypes.object.isRequired,
|
|
||||||
history: PropTypes.object.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { ConnectedRouter, ConnectedRouterProps } from 'connected-react-router';
|
||||||
|
import React from 'react';
|
||||||
|
import DocumentTitle from 'react-document-title';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { Store } from 'redux';
|
||||||
|
import Page from 'Components/Page/Page';
|
||||||
|
import ApplyTheme from './ApplyTheme';
|
||||||
|
import AppRoutes from './AppRoutes';
|
||||||
|
|
||||||
|
interface AppProps {
|
||||||
|
store: Store;
|
||||||
|
history: ConnectedRouterProps['history'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
|
function App({ store, history }: AppProps) {
|
||||||
|
return (
|
||||||
|
<DocumentTitle title={window.Radarr.instanceName}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<ConnectedRouter history={history}>
|
||||||
|
<ApplyTheme />
|
||||||
|
<Page>
|
||||||
|
<AppRoutes />
|
||||||
|
</Page>
|
||||||
|
</ConnectedRouter>
|
||||||
|
</Provider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
</DocumentTitle>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import { Redirect, Route } from 'react-router-dom';
|
|
||||||
import Blocklist from 'Activity/Blocklist/Blocklist';
|
|
||||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
|
||||||
import Queue from 'Activity/Queue/Queue';
|
|
||||||
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
|
||||||
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
|
||||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
|
||||||
import CollectionConnector from 'Collection/CollectionConnector';
|
|
||||||
import NotFound from 'Components/NotFound';
|
|
||||||
import Switch from 'Components/Router/Switch';
|
|
||||||
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
|
||||||
import MovieDetailsPageConnector from 'Movie/Details/MovieDetailsPageConnector';
|
|
||||||
import MovieIndex from 'Movie/Index/MovieIndex';
|
|
||||||
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
|
||||||
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
|
||||||
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
|
||||||
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
|
||||||
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
|
||||||
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
|
||||||
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
|
||||||
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
|
||||||
import Profiles from 'Settings/Profiles/Profiles';
|
|
||||||
import QualityConnector from 'Settings/Quality/QualityConnector';
|
|
||||||
import Settings from 'Settings/Settings';
|
|
||||||
import TagSettings from 'Settings/Tags/TagSettings';
|
|
||||||
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
|
||||||
import BackupsConnector from 'System/Backup/BackupsConnector';
|
|
||||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
|
||||||
import Logs from 'System/Logs/Logs';
|
|
||||||
import Status from 'System/Status/Status';
|
|
||||||
import Tasks from 'System/Tasks/Tasks';
|
|
||||||
import Updates from 'System/Updates/Updates';
|
|
||||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
|
||||||
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
|
|
||||||
import MissingConnector from 'Wanted/Missing/MissingConnector';
|
|
||||||
|
|
||||||
function AppRoutes(props) {
|
|
||||||
const {
|
|
||||||
app
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
{/*
|
|
||||||
Movies
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
component={MovieIndex}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{
|
|
||||||
window.Radarr.urlBase &&
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/"
|
|
||||||
addUrlBase={false}
|
|
||||||
render={() => {
|
|
||||||
return (
|
|
||||||
<Redirect
|
|
||||||
to={getPathWithUrlBase('/')}
|
|
||||||
component={app}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/new"
|
|
||||||
component={AddNewMovieConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/collections"
|
|
||||||
component={CollectionConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/import"
|
|
||||||
component={ImportMovies}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/add/discover"
|
|
||||||
component={DiscoverMovieConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/movie/:titleSlug"
|
|
||||||
component={MovieDetailsPageConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Calendar
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/calendar"
|
|
||||||
component={CalendarPageConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Activity
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/history"
|
|
||||||
component={HistoryConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/queue"
|
|
||||||
component={Queue}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/activity/blocklist"
|
|
||||||
component={Blocklist}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Wanted
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/missing"
|
|
||||||
component={MissingConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/wanted/cutoffunmet"
|
|
||||||
component={CutoffUnmetConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Settings
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
exact={true}
|
|
||||||
path="/settings"
|
|
||||||
component={Settings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/mediamanagement"
|
|
||||||
component={MediaManagementConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/profiles"
|
|
||||||
component={Profiles}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/quality"
|
|
||||||
component={QualityConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/customformats"
|
|
||||||
component={CustomFormatSettingsPage}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/indexers"
|
|
||||||
component={IndexerSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/downloadclients"
|
|
||||||
component={DownloadClientSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/importlists"
|
|
||||||
component={ImportListSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/connect"
|
|
||||||
component={NotificationSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/metadata"
|
|
||||||
component={MetadataSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/tags"
|
|
||||||
component={TagSettings}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/general"
|
|
||||||
component={GeneralSettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/settings/ui"
|
|
||||||
component={UISettingsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
System
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/status"
|
|
||||||
component={Status}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/tasks"
|
|
||||||
component={Tasks}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/backup"
|
|
||||||
component={BackupsConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/updates"
|
|
||||||
component={Updates}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/events"
|
|
||||||
component={LogsTableConnector}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="/system/logs/files"
|
|
||||||
component={Logs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/*
|
|
||||||
Not Found
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Route
|
|
||||||
path="*"
|
|
||||||
component={NotFound}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppRoutes.propTypes = {
|
|
||||||
app: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppRoutes;
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
|
import Blocklist from 'Activity/Blocklist/Blocklist';
|
||||||
|
import History from 'Activity/History/History';
|
||||||
|
import Queue from 'Activity/Queue/Queue';
|
||||||
|
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
|
||||||
|
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
|
||||||
|
import CalendarPage from 'Calendar/CalendarPage';
|
||||||
|
import CollectionConnector from 'Collection/CollectionConnector';
|
||||||
|
import NotFound from 'Components/NotFound';
|
||||||
|
import Switch from 'Components/Router/Switch';
|
||||||
|
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
|
||||||
|
import MovieDetailsPage from 'Movie/Details/MovieDetailsPage';
|
||||||
|
import MovieIndex from 'Movie/Index/MovieIndex';
|
||||||
|
import CustomFormatSettingsPage from 'Settings/CustomFormats/CustomFormatSettingsPage';
|
||||||
|
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
|
||||||
|
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
|
||||||
|
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
|
||||||
|
import IndexerSettingsConnector from 'Settings/Indexers/IndexerSettingsConnector';
|
||||||
|
import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementConnector';
|
||||||
|
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
|
||||||
|
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
|
||||||
|
import Profiles from 'Settings/Profiles/Profiles';
|
||||||
|
import QualityConnector from 'Settings/Quality/QualityConnector';
|
||||||
|
import Settings from 'Settings/Settings';
|
||||||
|
import TagSettings from 'Settings/Tags/TagSettings';
|
||||||
|
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
|
||||||
|
import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||||
|
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||||
|
import Logs from 'System/Logs/Logs';
|
||||||
|
import Status from 'System/Status/Status';
|
||||||
|
import Tasks from 'System/Tasks/Tasks';
|
||||||
|
import Updates from 'System/Updates/Updates';
|
||||||
|
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||||
|
import CutoffUnmet from 'Wanted/CutoffUnmet/CutoffUnmet';
|
||||||
|
import Missing from 'Wanted/Missing/Missing';
|
||||||
|
|
||||||
|
function RedirectWithUrlBase() {
|
||||||
|
return <Redirect to={getPathWithUrlBase('/')} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppRoutes() {
|
||||||
|
return (
|
||||||
|
<Switch>
|
||||||
|
{/*
|
||||||
|
Movies
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/" component={MovieIndex} />
|
||||||
|
|
||||||
|
{window.Radarr.urlBase && (
|
||||||
|
<Route
|
||||||
|
exact={true}
|
||||||
|
path="/"
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
addUrlBase={false}
|
||||||
|
render={RedirectWithUrlBase}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Route path="/add/new" component={AddNewMovieConnector} />
|
||||||
|
|
||||||
|
<Route path="/collections" component={CollectionConnector} />
|
||||||
|
|
||||||
|
<Route path="/add/import" component={ImportMovies} />
|
||||||
|
|
||||||
|
<Route path="/add/discover" component={DiscoverMovieConnector} />
|
||||||
|
|
||||||
|
<Route path="/movie/:titleSlug" component={MovieDetailsPage} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Calendar
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/calendar" component={CalendarPage} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Activity
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/activity/history" component={History} />
|
||||||
|
|
||||||
|
<Route path="/activity/queue" component={Queue} />
|
||||||
|
|
||||||
|
<Route path="/activity/blocklist" component={Blocklist} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Wanted
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/wanted/missing" component={Missing} />
|
||||||
|
|
||||||
|
<Route path="/wanted/cutoffunmet" component={CutoffUnmet} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Settings
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route exact={true} path="/settings" component={Settings} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/mediamanagement"
|
||||||
|
component={MediaManagementConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/profiles" component={Profiles} />
|
||||||
|
|
||||||
|
<Route path="/settings/quality" component={QualityConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/customformats"
|
||||||
|
component={CustomFormatSettingsPage}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/downloadclients"
|
||||||
|
component={DownloadClientSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/settings/importlists"
|
||||||
|
component={ImportListSettingsConnector}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Route path="/settings/connect" component={NotificationSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/metadata" component={MetadataSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/tags" component={TagSettings} />
|
||||||
|
|
||||||
|
<Route path="/settings/general" component={GeneralSettingsConnector} />
|
||||||
|
|
||||||
|
<Route path="/settings/ui" component={UISettingsConnector} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
System
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="/system/status" component={Status} />
|
||||||
|
|
||||||
|
<Route path="/system/tasks" component={Tasks} />
|
||||||
|
|
||||||
|
<Route path="/system/backup" component={BackupsConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/updates" component={Updates} />
|
||||||
|
|
||||||
|
<Route path="/system/events" component={LogsTableConnector} />
|
||||||
|
|
||||||
|
<Route path="/system/logs/files" component={Logs} />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
Not Found
|
||||||
|
*/}
|
||||||
|
|
||||||
|
<Route path="*" component={NotFound} />
|
||||||
|
</Switch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppRoutes;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import AppUpdatedModalContentConnector from './AppUpdatedModalContentConnector';
|
|
||||||
|
|
||||||
function AppUpdatedModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnBackgroundClick={false}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<AppUpdatedModalContentConnector
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModal;
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
||||||
|
|
||||||
|
interface AppUpdatedModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onModalClose: (...args: unknown[]) => unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModal(props: AppUpdatedModalProps) {
|
||||||
|
const { isOpen, onModalClose } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
>
|
||||||
|
<AppUpdatedModalContent onModalClose={handleModalClose} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import AppUpdatedModal from './AppUpdatedModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(AppUpdatedModal);
|
|
||||||
@@ -1,139 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import UpdateChanges from 'System/Updates/UpdateChanges';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AppUpdatedModalContent.css';
|
|
||||||
|
|
||||||
function mergeUpdates(items, version, prevVersion) {
|
|
||||||
let installedIndex = items.findIndex((u) => u.version === version);
|
|
||||||
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
|
|
||||||
|
|
||||||
if (installedIndex === -1) {
|
|
||||||
installedIndex = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (installedPreviouslyIndex === -1) {
|
|
||||||
installedPreviouslyIndex = items.length;
|
|
||||||
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
|
||||||
installedPreviouslyIndex += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
|
||||||
|
|
||||||
if (!appliedUpdates.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const appliedChanges = { new: [], fixed: [] };
|
|
||||||
appliedUpdates.forEach((u) => {
|
|
||||||
if (u.changes) {
|
|
||||||
appliedChanges.new.push(... u.changes.new);
|
|
||||||
appliedChanges.fixed.push(... u.changes.fixed);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
|
|
||||||
|
|
||||||
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
|
||||||
mergedUpdate.changes = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedUpdate;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AppUpdatedModalContent(props) {
|
|
||||||
const {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items,
|
|
||||||
onSeeChangesPress,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const update = mergeUpdates(items, version, prevVersion);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('AppUpdated')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<div>
|
|
||||||
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && !error && !!update &&
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
!update.changes &&
|
|
||||||
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!update.changes &&
|
|
||||||
<div>
|
|
||||||
<div className={styles.changes}>
|
|
||||||
{translate('WhatsNew')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('New')}
|
|
||||||
changes={update.changes.new}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<UpdateChanges
|
|
||||||
title={translate('Fixed')}
|
|
||||||
changes={update.changes.fixed}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isPopulated && !error &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onSeeChangesPress}
|
|
||||||
>
|
|
||||||
{translate('RecentChanges')}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContent.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
prevVersion: PropTypes.string,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
onSeeChangesPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppUpdatedModalContent;
|
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
import React, { useCallback, useEffect } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import { fetchUpdates } from 'Store/Actions/systemActions';
|
||||||
|
import UpdateChanges from 'System/Updates/UpdateChanges';
|
||||||
|
import Update from 'typings/Update';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import AppState from './State/AppState';
|
||||||
|
import styles from './AppUpdatedModalContent.css';
|
||||||
|
|
||||||
|
function mergeUpdates(items: Update[], version: string, prevVersion?: string) {
|
||||||
|
let installedIndex = items.findIndex((u) => u.version === version);
|
||||||
|
let installedPreviouslyIndex = items.findIndex(
|
||||||
|
(u) => u.version === prevVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
if (installedIndex === -1) {
|
||||||
|
installedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installedPreviouslyIndex === -1) {
|
||||||
|
installedPreviouslyIndex = items.length;
|
||||||
|
} else if (installedPreviouslyIndex === installedIndex && items.length) {
|
||||||
|
installedPreviouslyIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
|
||||||
|
|
||||||
|
if (!appliedUpdates.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appliedChanges: Update['changes'] = { new: [], fixed: [] };
|
||||||
|
|
||||||
|
appliedUpdates.forEach((u: Update) => {
|
||||||
|
if (u.changes) {
|
||||||
|
appliedChanges.new.push(...u.changes.new);
|
||||||
|
appliedChanges.fixed.push(...u.changes.fixed);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mergedUpdate: Update = Object.assign({}, appliedUpdates[0], {
|
||||||
|
changes: appliedChanges,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
|
||||||
|
mergedUpdate.changes = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergedUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppUpdatedModalContentProps {
|
||||||
|
onModalClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AppUpdatedModalContent(props: AppUpdatedModalContentProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { version, prevVersion } = useSelector((state: AppState) => state.app);
|
||||||
|
const { isPopulated, error, items } = useSelector(
|
||||||
|
(state: AppState) => state.system.updates
|
||||||
|
);
|
||||||
|
const previousVersion = usePrevious(version);
|
||||||
|
|
||||||
|
const { onModalClose } = props;
|
||||||
|
|
||||||
|
const update = mergeUpdates(items, version, prevVersion);
|
||||||
|
|
||||||
|
const handleSeeChangesPress = useCallback(() => {
|
||||||
|
window.location.href = `${window.Radarr.urlBase}/system/updates`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (version !== previousVersion) {
|
||||||
|
dispatch(fetchUpdates());
|
||||||
|
}
|
||||||
|
}, [version, previousVersion, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>{translate('AppUpdated')}</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<div>
|
||||||
|
<InlineMarkdown
|
||||||
|
data={translate('AppUpdatedVersion', { version })}
|
||||||
|
blockClassName={styles.version}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isPopulated && !error && !!update ? (
|
||||||
|
<div>
|
||||||
|
{update.changes ? (
|
||||||
|
<div className={styles.maintenance}>
|
||||||
|
{translate('MaintenanceRelease')}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{update.changes ? (
|
||||||
|
<div>
|
||||||
|
<div className={styles.changes}>{translate('WhatsNew')}</div>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('New')}
|
||||||
|
changes={update.changes.new}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UpdateChanges
|
||||||
|
title={translate('Fixed')}
|
||||||
|
changes={update.changes.fixed}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!isPopulated && !error ? <LoadingIndicator /> : null}
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<Button onPress={handleSeeChangesPress}>
|
||||||
|
{translate('RecentChanges')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button kind={kinds.PRIMARY} onPress={onModalClose}>
|
||||||
|
{translate('Reload')}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AppUpdatedModalContent;
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { fetchUpdates } from 'Store/Actions/systemActions';
|
|
||||||
import AppUpdatedModalContent from './AppUpdatedModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.app.version,
|
|
||||||
(state) => state.app.prevVersion,
|
|
||||||
(state) => state.system.updates,
|
|
||||||
(version, prevVersion, updates) => {
|
|
||||||
const {
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
} = updates;
|
|
||||||
|
|
||||||
return {
|
|
||||||
version,
|
|
||||||
prevVersion,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
items
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
dispatchFetchUpdates() {
|
|
||||||
dispatch(fetchUpdates());
|
|
||||||
},
|
|
||||||
|
|
||||||
onSeeChangesPress() {
|
|
||||||
window.location = `${window.Radarr.urlBase}/system/updates`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
class AppUpdatedModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.version !== this.props.version) {
|
|
||||||
this.props.dispatchFetchUpdates();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dispatchFetchUpdates,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppUpdatedModalContent {...otherProps} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppUpdatedModalContentConnector.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
dispatchFetchUpdates: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, createMapDispatchToProps)(AppUpdatedModalContentConnector);
|
|
||||||
+16
-27
@@ -1,5 +1,4 @@
|
|||||||
import PropTypes from 'prop-types';
|
import React, { useCallback } from 'react';
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
@@ -10,36 +9,31 @@ import { kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ConnectionLostModal.css';
|
import styles from './ConnectionLostModal.css';
|
||||||
|
|
||||||
function ConnectionLostModal(props) {
|
interface ConnectionLostModalProps {
|
||||||
const {
|
isOpen: boolean;
|
||||||
isOpen,
|
}
|
||||||
onModalClose
|
|
||||||
} = props;
|
function ConnectionLostModal(props: ConnectionLostModalProps) {
|
||||||
|
const { isOpen } = props;
|
||||||
|
|
||||||
|
const handleModalClose = useCallback(() => {
|
||||||
|
location.reload();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
|
||||||
isOpen={isOpen}
|
<ModalContent onModalClose={handleModalClose}>
|
||||||
onModalClose={onModalClose}
|
<ModalHeader>{translate('ConnectionLost')}</ModalHeader>
|
||||||
>
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
{translate('ConnectionLost')}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>{translate('ConnectionLostToBackend')}</div>
|
||||||
{translate('ConnectionLostToBackend')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostReconnect')}
|
{translate('ConnectionLostReconnect')}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button kind={kinds.PRIMARY} onPress={handleModalClose}>
|
||||||
kind={kinds.PRIMARY}
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
{translate('Reload')}
|
{translate('Reload')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
@@ -48,9 +42,4 @@ function ConnectionLostModal(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ConnectionLostModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ConnectionLostModal;
|
export default ConnectionLostModal;
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ConnectionLostModal from './ConnectionLostModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(undefined, createMapDispatchToProps)(ConnectionLostModal);
|
|
||||||
@@ -9,13 +9,13 @@ export type SelectContextAction =
|
|||||||
| { type: 'unselectAll' }
|
| { type: 'unselectAll' }
|
||||||
| {
|
| {
|
||||||
type: 'toggleSelected';
|
type: 'toggleSelected';
|
||||||
id: number;
|
id: number | string;
|
||||||
isSelected: boolean;
|
isSelected: boolean | null;
|
||||||
shiftKey: boolean;
|
shiftKey: boolean;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'removeItem';
|
type: 'removeItem';
|
||||||
id: number;
|
id: number | string;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: 'updateItems';
|
type: 'updateItems';
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import { FilterBuilderProp, PropertyFilter } from './AppState';
|
import { ValidationFailure } from 'typings/pending';
|
||||||
|
import { Filter, FilterBuilderProp } 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 {
|
||||||
@@ -30,7 +35,7 @@ export interface TableAppSectionState {
|
|||||||
|
|
||||||
export interface AppSectionFilterState<T> {
|
export interface AppSectionFilterState<T> {
|
||||||
selectedFilterKey: string;
|
selectedFilterKey: string;
|
||||||
filters: PropertyFilter[];
|
filters: Filter[];
|
||||||
filterBuilderProps: FilterBuilderProp<T>[];
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,18 +1,30 @@
|
|||||||
|
import { Error } from './AppSectionState';
|
||||||
import BlocklistAppState from './BlocklistAppState';
|
import BlocklistAppState from './BlocklistAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
|
import CaptchaAppState from './CaptchaAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
import HistoryAppState from './HistoryAppState';
|
import CustomFiltersAppState from './CustomFiltersAppState';
|
||||||
|
import ExtraFilesAppState from './ExtraFilesAppState';
|
||||||
|
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
|
||||||
import InteractiveImportAppState from './InteractiveImportAppState';
|
import InteractiveImportAppState from './InteractiveImportAppState';
|
||||||
|
import MessagesAppState from './MessagesAppState';
|
||||||
|
import MovieBlocklistAppState from './MovieBlocklistAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
import MovieCreditAppState from './MovieCreditAppState';
|
import MovieCreditAppState from './MovieCreditAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
|
import OAuthAppState from './OAuthAppState';
|
||||||
|
import OrganizePreviewAppState from './OrganizePreviewAppState';
|
||||||
import ParseAppState from './ParseAppState';
|
import ParseAppState from './ParseAppState';
|
||||||
|
import PathsAppState from './PathsAppState';
|
||||||
|
import ProviderOptionsAppState from './ProviderOptionsAppState';
|
||||||
import QueueAppState from './QueueAppState';
|
import QueueAppState from './QueueAppState';
|
||||||
|
import ReleasesAppState from './ReleasesAppState';
|
||||||
import RootFolderAppState from './RootFolderAppState';
|
import RootFolderAppState from './RootFolderAppState';
|
||||||
import SettingsAppState from './SettingsAppState';
|
import SettingsAppState from './SettingsAppState';
|
||||||
import SystemAppState from './SystemAppState';
|
import SystemAppState from './SystemAppState';
|
||||||
import TagsAppState from './TagsAppState';
|
import TagsAppState from './TagsAppState';
|
||||||
|
import WantedAppState from './WantedAppState';
|
||||||
|
|
||||||
interface FilterBuilderPropOption {
|
interface FilterBuilderPropOption {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -35,46 +47,67 @@ export interface PropertyFilter {
|
|||||||
|
|
||||||
export interface Filter {
|
export interface Filter {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string | (() => string);
|
||||||
filers: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CustomFilter {
|
export interface CustomFilter {
|
||||||
id: number;
|
id: number;
|
||||||
type: string;
|
type: string;
|
||||||
label: string;
|
label: string;
|
||||||
filers: PropertyFilter[];
|
filters: PropertyFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppSectionState {
|
export interface AppSectionState {
|
||||||
|
isUpdated: boolean;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
|
isDisconnected: boolean;
|
||||||
isReconnecting: boolean;
|
isReconnecting: boolean;
|
||||||
|
isSidebarVisible: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
|
prevVersion?: string;
|
||||||
dimensions: {
|
dimensions: {
|
||||||
isSmallScreen: boolean;
|
isSmallScreen: boolean;
|
||||||
|
isLargeScreen: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
|
translations: {
|
||||||
|
error?: Error;
|
||||||
|
isPopulated: boolean;
|
||||||
|
};
|
||||||
|
messages: MessagesAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
app: AppSectionState;
|
app: AppSectionState;
|
||||||
blocklist: BlocklistAppState;
|
blocklist: BlocklistAppState;
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
|
captcha: CaptchaAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
|
customFilters: CustomFiltersAppState;
|
||||||
|
extraFiles: ExtraFilesAppState;
|
||||||
history: HistoryAppState;
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
|
movieBlocklist: MovieBlocklistAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
movieCredits: MovieCreditAppState;
|
movieCredits: MovieCreditAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
|
movieHistory: MovieHistoryAppState;
|
||||||
movieIndex: MovieIndexAppState;
|
movieIndex: MovieIndexAppState;
|
||||||
movies: MoviesAppState;
|
movies: MoviesAppState;
|
||||||
|
oAuth: OAuthAppState;
|
||||||
|
organizePreview: OrganizePreviewAppState;
|
||||||
parse: ParseAppState;
|
parse: ParseAppState;
|
||||||
|
paths: PathsAppState;
|
||||||
|
providerOptions: ProviderOptionsAppState;
|
||||||
queue: QueueAppState;
|
queue: QueueAppState;
|
||||||
|
releases: ReleasesAppState;
|
||||||
rootFolders: RootFolderAppState;
|
rootFolders: RootFolderAppState;
|
||||||
settings: SettingsAppState;
|
settings: SettingsAppState;
|
||||||
system: SystemAppState;
|
system: SystemAppState;
|
||||||
tags: TagsAppState;
|
tags: TagsAppState;
|
||||||
|
wanted: WantedAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default AppState;
|
export default AppState;
|
||||||
|
|||||||
@@ -1,10 +1,29 @@
|
|||||||
|
import moment from 'moment';
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Movie from 'Movie/Movie';
|
import { CalendarView } from 'Calendar/calendarViews';
|
||||||
|
import { CalendarItem } from 'typings/Calendar';
|
||||||
|
|
||||||
|
interface CalendarOptions {
|
||||||
|
showMovieInformation: boolean;
|
||||||
|
showCinemaRelease: boolean;
|
||||||
|
showDigitalRelease: boolean;
|
||||||
|
showPhysicalRelease: boolean;
|
||||||
|
showCutoffUnmetIcon: boolean;
|
||||||
|
fullColorEvents: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
interface CalendarAppState
|
interface CalendarAppState
|
||||||
extends AppSectionState<Movie>,
|
extends AppSectionState<CalendarItem>,
|
||||||
AppSectionFilterState<Movie> {}
|
AppSectionFilterState<CalendarItem> {
|
||||||
|
searchMissingCommandId: number | null;
|
||||||
|
start: moment.Moment;
|
||||||
|
end: moment.Moment;
|
||||||
|
dates: string[];
|
||||||
|
time: string;
|
||||||
|
view: CalendarView;
|
||||||
|
options: CalendarOptions;
|
||||||
|
}
|
||||||
|
|
||||||
export default CalendarAppState;
|
export default CalendarAppState;
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
interface CaptchaAppState {
|
||||||
|
refreshing: false;
|
||||||
|
token: string;
|
||||||
|
siteKey: unknown;
|
||||||
|
secretToken: unknown;
|
||||||
|
ray: unknown;
|
||||||
|
stoken: unknown;
|
||||||
|
responseUrl: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CaptchaAppState;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import { ExtraFile } from 'MovieFile/ExtraFile';
|
||||||
|
|
||||||
|
type ExtraFilesAppState = AppSectionState<ExtraFile>;
|
||||||
|
|
||||||
|
export default ExtraFilesAppState;
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionFilterState,
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import History from 'typings/History';
|
import History from 'typings/History';
|
||||||
|
|
||||||
|
export type MovieHistoryAppState = AppSectionState<History>;
|
||||||
|
|
||||||
interface HistoryAppState
|
interface HistoryAppState
|
||||||
extends AppSectionState<History>,
|
extends AppSectionState<History>,
|
||||||
AppSectionFilterState<History> {}
|
AppSectionFilterState<History>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
export default HistoryAppState;
|
export default HistoryAppState;
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import RecentFolder from 'InteractiveImport/Folder/RecentFolder';
|
|
||||||
import ImportMode from 'InteractiveImport/ImportMode';
|
import ImportMode from 'InteractiveImport/ImportMode';
|
||||||
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
import InteractiveImport from 'InteractiveImport/InteractiveImport';
|
||||||
|
|
||||||
|
interface FavoriteFolder {
|
||||||
|
folder: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecentFolder {
|
||||||
|
folder: string;
|
||||||
|
lastUsed: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
interface InteractiveImportAppState extends AppSectionState<InteractiveImport> {
|
||||||
originalItems: InteractiveImport[];
|
originalItems: InteractiveImport[];
|
||||||
importMode: ImportMode;
|
importMode: ImportMode;
|
||||||
|
favoriteFolders: FavoriteFolder[];
|
||||||
recentFolders: RecentFolder[];
|
recentFolders: RecentFolder[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
|
||||||
|
export type MessageType = 'error' | 'info' | 'success' | 'warning';
|
||||||
|
|
||||||
|
export interface Message extends ModelBase {
|
||||||
|
hideAfter: number;
|
||||||
|
message: string;
|
||||||
|
name: string;
|
||||||
|
type: MessageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
type MessagesAppState = AppSectionState<Message>;
|
||||||
|
|
||||||
|
export default MessagesAppState;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { AppSectionProviderState } from 'App/State/AppSectionState';
|
||||||
|
import Metadata from 'typings/Metadata';
|
||||||
|
|
||||||
|
type MetadataAppState = AppSectionProviderState<Metadata>;
|
||||||
|
|
||||||
|
export default MetadataAppState;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Blocklist from 'typings/Blocklist';
|
||||||
|
|
||||||
|
type MovieBlocklistAppState = AppSectionState<Blocklist>;
|
||||||
|
|
||||||
|
export default MovieBlocklistAppState;
|
||||||
@@ -1,8 +1,20 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
AppSectionSaveState,
|
||||||
|
Error,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
import MovieCollection from 'typings/MovieCollection';
|
import MovieCollection from 'typings/MovieCollection';
|
||||||
|
|
||||||
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
interface MovieCollectionAppState
|
||||||
|
extends AppSectionState<MovieCollection>,
|
||||||
|
AppSectionFilterState<MovieCollection>,
|
||||||
|
AppSectionSaveState {
|
||||||
itemMap: Record<number, number>;
|
itemMap: Record<number, number>;
|
||||||
|
|
||||||
|
isAdding: boolean;
|
||||||
|
addError: Error;
|
||||||
|
|
||||||
|
pendingChanges: Partial<MovieCollection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MovieCollectionAppState;
|
export default MovieCollectionAppState;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import AppSectionState, {
|
|||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
} from 'App/State/AppSectionState';
|
} from 'App/State/AppSectionState';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import { Filter, FilterBuilderProp } from './AppState';
|
import { Filter, FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
@@ -64,6 +64,8 @@ interface MoviesAppState
|
|||||||
deleteOptions: {
|
deleteOptions: {
|
||||||
addImportExclusion: boolean;
|
addImportExclusion: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pendingChanges: Partial<Movie>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MoviesAppState;
|
export default MoviesAppState;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { Error } from './AppSectionState';
|
||||||
|
|
||||||
|
interface OAuthAppState {
|
||||||
|
authorizing: boolean;
|
||||||
|
result: Record<string, unknown> | null;
|
||||||
|
error: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OAuthAppState;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import ModelBase from 'App/ModelBase';
|
||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
|
||||||
|
export interface OrganizePreviewModel extends ModelBase {
|
||||||
|
movieId: number;
|
||||||
|
movieFileId: number;
|
||||||
|
existingPath: string;
|
||||||
|
newPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OrganizePreviewAppState = AppSectionState<OrganizePreviewModel>;
|
||||||
|
|
||||||
|
export default OrganizePreviewAppState;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
interface BasePath {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface File extends BasePath {
|
||||||
|
type: 'file';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Folder extends BasePath {
|
||||||
|
type: 'folder';
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PathType = 'file' | 'folder' | 'drive' | 'computer' | 'parent';
|
||||||
|
export type Path = File | Folder;
|
||||||
|
|
||||||
|
interface PathsAppState {
|
||||||
|
currentPath: string;
|
||||||
|
isFetching: boolean;
|
||||||
|
isPopulated: boolean;
|
||||||
|
error: Error;
|
||||||
|
directories: Folder[];
|
||||||
|
files: File[];
|
||||||
|
parent: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PathsAppState;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
|
import Field, { FieldSelectOption } from 'typings/Field';
|
||||||
|
|
||||||
|
export interface ProviderOptions {
|
||||||
|
fields?: Field[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsDevice {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProviderOptionsAppState {
|
||||||
|
devices: AppSectionState<ProviderOptionsDevice>;
|
||||||
|
servers: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
newznabCategories: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getProfiles: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getTags: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
getRootFolders: AppSectionState<FieldSelectOption<unknown>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProviderOptionsAppState;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import Release from 'typings/Release';
|
||||||
|
|
||||||
|
interface ReleasesAppState
|
||||||
|
extends AppSectionState<Release>,
|
||||||
|
AppSectionFilterState<Release> {}
|
||||||
|
|
||||||
|
export default ReleasesAppState;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import DiskSpace from 'typings/DiskSpace';
|
import DiskSpace from 'typings/DiskSpace';
|
||||||
import Health from 'typings/Health';
|
import Health from 'typings/Health';
|
||||||
|
import LogFile from 'typings/LogFile';
|
||||||
import SystemStatus from 'typings/SystemStatus';
|
import SystemStatus from 'typings/SystemStatus';
|
||||||
import Task from 'typings/Task';
|
import Task from 'typings/Task';
|
||||||
import Update from 'typings/Update';
|
import Update from 'typings/Update';
|
||||||
@@ -9,13 +10,16 @@ export type DiskSpaceAppState = AppSectionState<DiskSpace>;
|
|||||||
export type HealthAppState = AppSectionState<Health>;
|
export type HealthAppState = AppSectionState<Health>;
|
||||||
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
|
||||||
export type TaskAppState = AppSectionState<Task>;
|
export type TaskAppState = AppSectionState<Task>;
|
||||||
|
export type LogFilesAppState = AppSectionState<LogFile>;
|
||||||
export type UpdateAppState = AppSectionState<Update>;
|
export type UpdateAppState = AppSectionState<Update>;
|
||||||
|
|
||||||
interface SystemAppState {
|
interface SystemAppState {
|
||||||
diskSpace: DiskSpaceAppState;
|
diskSpace: DiskSpaceAppState;
|
||||||
health: HealthAppState;
|
health: HealthAppState;
|
||||||
|
logFiles: LogFilesAppState;
|
||||||
status: SystemStatusAppState;
|
status: SystemStatusAppState;
|
||||||
tasks: TaskAppState;
|
tasks: TaskAppState;
|
||||||
|
updateLogFiles: LogFilesAppState;
|
||||||
updates: UpdateAppState;
|
updates: UpdateAppState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
|
||||||
|
interface WantedMovie extends Movie {
|
||||||
|
isSaving?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WantedCutoffUnmetAppState
|
||||||
|
extends AppSectionState<WantedMovie>,
|
||||||
|
AppSectionFilterState<WantedMovie>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
|
interface WantedMissingAppState
|
||||||
|
extends AppSectionState<WantedMovie>,
|
||||||
|
AppSectionFilterState<WantedMovie>,
|
||||||
|
PagedAppSectionState,
|
||||||
|
TableAppSectionState {}
|
||||||
|
|
||||||
|
interface WantedAppState {
|
||||||
|
cutoffUnmet: WantedCutoffUnmetAppState;
|
||||||
|
missing: WantedMissingAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WantedAppState;
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import AgendaEventConnector from './AgendaEventConnector';
|
|
||||||
import styles from './Agenda.css';
|
|
||||||
|
|
||||||
function Agenda(props) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
start,
|
|
||||||
end
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const startDateParsed = Date.parse(start);
|
|
||||||
const endDateParsed = Date.parse(end);
|
|
||||||
|
|
||||||
items.forEach((item) => {
|
|
||||||
const cinemaDateParsed = Date.parse(item.inCinemas);
|
|
||||||
const digitalDateParsed = Date.parse(item.digitalRelease);
|
|
||||||
const physicalDateParsed = Date.parse(item.physicalRelease);
|
|
||||||
const dates = [];
|
|
||||||
|
|
||||||
if (cinemaDateParsed > 0 && cinemaDateParsed >= startDateParsed && cinemaDateParsed <= endDateParsed) {
|
|
||||||
dates.push(cinemaDateParsed);
|
|
||||||
}
|
|
||||||
if (digitalDateParsed > 0 && digitalDateParsed >= startDateParsed && digitalDateParsed <= endDateParsed) {
|
|
||||||
dates.push(digitalDateParsed);
|
|
||||||
}
|
|
||||||
if (physicalDateParsed > 0 && physicalDateParsed >= startDateParsed && physicalDateParsed <= endDateParsed) {
|
|
||||||
dates.push(physicalDateParsed);
|
|
||||||
}
|
|
||||||
|
|
||||||
item.sortDate = Math.min(...dates);
|
|
||||||
item.cinemaDateParsed = cinemaDateParsed;
|
|
||||||
item.digitalDateParsed = digitalDateParsed;
|
|
||||||
item.physicalDateParsed = physicalDateParsed;
|
|
||||||
});
|
|
||||||
|
|
||||||
items.sort((a, b) => ((a.sortDate > b.sortDate) ? 1 : -1));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.agenda}>
|
|
||||||
{
|
|
||||||
items.map((item, index) => {
|
|
||||||
const momentDate = moment(item.sortDate);
|
|
||||||
const showDate = index === 0 ||
|
|
||||||
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AgendaEventConnector
|
|
||||||
key={item.id}
|
|
||||||
movieId={item.id}
|
|
||||||
showDate={showDate}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Agenda.propTypes = {
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
start: PropTypes.string.isRequired,
|
|
||||||
end: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Agenda;
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import AgendaEvent from './AgendaEvent';
|
||||||
|
import styles from './Agenda.css';
|
||||||
|
|
||||||
|
interface AgendaMovie extends Movie {
|
||||||
|
sortDate: moment.Moment;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Agenda() {
|
||||||
|
const { start, end, items } = useSelector(
|
||||||
|
(state: AppState) => state.calendar
|
||||||
|
);
|
||||||
|
|
||||||
|
const events = useMemo(() => {
|
||||||
|
const result = items.map((item): AgendaMovie => {
|
||||||
|
const { inCinemas, digitalRelease, physicalRelease } = item;
|
||||||
|
|
||||||
|
const dates = [];
|
||||||
|
|
||||||
|
if (inCinemas) {
|
||||||
|
const inCinemasMoment = moment(inCinemas);
|
||||||
|
|
||||||
|
if (inCinemasMoment.isAfter(start) && inCinemasMoment.isBefore(end)) {
|
||||||
|
dates.push(inCinemasMoment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digitalRelease) {
|
||||||
|
const digitalReleaseMoment = moment(digitalRelease);
|
||||||
|
|
||||||
|
if (
|
||||||
|
digitalReleaseMoment.isAfter(start) &&
|
||||||
|
digitalReleaseMoment.isBefore(end)
|
||||||
|
) {
|
||||||
|
dates.push(digitalReleaseMoment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (physicalRelease) {
|
||||||
|
const physicalReleaseMoment = moment(physicalRelease);
|
||||||
|
|
||||||
|
if (
|
||||||
|
physicalReleaseMoment.isAfter(start) &&
|
||||||
|
physicalReleaseMoment.isBefore(end)
|
||||||
|
) {
|
||||||
|
dates.push(physicalReleaseMoment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortDate = moment.min(...dates);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
sortDate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
result.sort((a, b) => (a.sortDate > b.sortDate ? 1 : -1));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [items, start, end]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.agenda}>
|
||||||
|
{events.map((item, index) => {
|
||||||
|
const momentDate = moment(item.sortDate);
|
||||||
|
const showDate =
|
||||||
|
index === 0 ||
|
||||||
|
!moment(events[index - 1].sortDate).isSame(momentDate, 'day');
|
||||||
|
|
||||||
|
return <AgendaEvent key={item.id} showDate={showDate} {...item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Agenda;
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import Agenda from './Agenda';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
(calendar) => {
|
|
||||||
return calendar;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(Agenda);
|
|
||||||
@@ -53,6 +53,13 @@
|
|||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.releaseIcon {
|
||||||
|
margin-right: 20px;
|
||||||
|
width: 25px;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -107,8 +114,3 @@
|
|||||||
flex: 0 0 100%;
|
flex: 0 0 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.releaseIcon {
|
|
||||||
margin-right: 20px;
|
|
||||||
width: 25px;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './AgendaEvent.css';
|
|
||||||
|
|
||||||
class AgendaEvent extends Component {
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isDetailsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onPress = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onDetailsModalClose = () => {
|
|
||||||
this.setState({ isDetailsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
movieFile,
|
|
||||||
title,
|
|
||||||
titleSlug,
|
|
||||||
genres,
|
|
||||||
isAvailable,
|
|
||||||
inCinemas,
|
|
||||||
digitalRelease,
|
|
||||||
physicalRelease,
|
|
||||||
monitored,
|
|
||||||
hasFile,
|
|
||||||
grabbed,
|
|
||||||
queueItem,
|
|
||||||
showDate,
|
|
||||||
showMovieInformation,
|
|
||||||
showCutoffUnmetIcon,
|
|
||||||
longDateFormat,
|
|
||||||
colorImpairedMode,
|
|
||||||
cinemaDateParsed,
|
|
||||||
digitalDateParsed,
|
|
||||||
physicalDateParsed,
|
|
||||||
sortDate
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let startTime = null;
|
|
||||||
let releaseIcon = null;
|
|
||||||
|
|
||||||
if (physicalDateParsed === sortDate) {
|
|
||||||
startTime = physicalRelease;
|
|
||||||
releaseIcon = icons.DISC;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (digitalDateParsed === sortDate) {
|
|
||||||
startTime = digitalRelease;
|
|
||||||
releaseIcon = icons.MOVIE_FILE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cinemaDateParsed === sortDate) {
|
|
||||||
startTime = inCinemas;
|
|
||||||
releaseIcon = icons.IN_CINEMAS;
|
|
||||||
}
|
|
||||||
|
|
||||||
startTime = moment(startTime);
|
|
||||||
const downloading = !!(queueItem || grabbed);
|
|
||||||
const isMonitored = monitored;
|
|
||||||
const statusStyle = getStatusStyle(hasFile, downloading, isMonitored, isAvailable);
|
|
||||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
|
||||||
const link = `/movie/${titleSlug}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.event}>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
to={link}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.overlay}>
|
|
||||||
<div className={styles.date}>
|
|
||||||
{showDate ? startTime.format(longDateFormat) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.releaseIcon}>
|
|
||||||
<Icon
|
|
||||||
name={releaseIcon}
|
|
||||||
kind={kinds.DEFAULT}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.eventWrapper,
|
|
||||||
styles[statusStyle],
|
|
||||||
colorImpairedMode && 'colorImpaired'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className={styles.movieTitle}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
showMovieInformation &&
|
|
||||||
<div className={styles.genres}>
|
|
||||||
{joinedGenres}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!!queueItem &&
|
|
||||||
<span className={styles.statusIcon}>
|
|
||||||
<CalendarEventQueueDetails
|
|
||||||
{...queueItem}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!queueItem && grabbed &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
title={translate('MovieIsDownloading')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet &&
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.MOVIE_FILE}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('QualityCutoffNotMet')}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AgendaEvent.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
movieFile: PropTypes.object,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
|
||||||
inCinemas: PropTypes.string,
|
|
||||||
digitalRelease: PropTypes.string,
|
|
||||||
physicalRelease: PropTypes.string,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
grabbed: PropTypes.bool,
|
|
||||||
queueItem: PropTypes.object,
|
|
||||||
showDate: PropTypes.bool.isRequired,
|
|
||||||
showMovieInformation: PropTypes.bool.isRequired,
|
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
colorImpairedMode: PropTypes.bool.isRequired,
|
|
||||||
cinemaDateParsed: PropTypes.number,
|
|
||||||
digitalDateParsed: PropTypes.number,
|
|
||||||
physicalDateParsed: PropTypes.number,
|
|
||||||
sortDate: PropTypes.number
|
|
||||||
};
|
|
||||||
|
|
||||||
AgendaEvent.defaultProps = {
|
|
||||||
genres: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AgendaEvent;
|
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
|
||||||
|
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import useMovieFile from 'MovieFile/useMovieFile';
|
||||||
|
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './AgendaEvent.css';
|
||||||
|
|
||||||
|
interface AgendaEventProps {
|
||||||
|
id: number;
|
||||||
|
movieFileId: number;
|
||||||
|
title: string;
|
||||||
|
titleSlug: string;
|
||||||
|
genres: string[];
|
||||||
|
inCinemas?: string;
|
||||||
|
digitalRelease?: string;
|
||||||
|
physicalRelease?: string;
|
||||||
|
sortDate: moment.Moment;
|
||||||
|
isAvailable: boolean;
|
||||||
|
monitored: boolean;
|
||||||
|
hasFile: boolean;
|
||||||
|
grabbed?: boolean;
|
||||||
|
showDate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AgendaEvent({
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
genres = [],
|
||||||
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
|
physicalRelease,
|
||||||
|
sortDate,
|
||||||
|
isAvailable,
|
||||||
|
monitored: isMonitored,
|
||||||
|
hasFile,
|
||||||
|
grabbed,
|
||||||
|
showDate,
|
||||||
|
}: AgendaEventProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
const queueItem = useSelector(createQueueItemSelectorForHook(id));
|
||||||
|
const { longDateFormat, enableColorImpairedMode } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
const { showMovieInformation, showCutoffUnmetIcon } = useSelector(
|
||||||
|
(state: AppState) => state.calendar.options
|
||||||
|
);
|
||||||
|
|
||||||
|
const { eventDate, eventTitle, releaseIcon } = useMemo(() => {
|
||||||
|
if (physicalRelease && sortDate.isSame(moment(physicalRelease), 'day')) {
|
||||||
|
return {
|
||||||
|
eventDate: physicalRelease,
|
||||||
|
eventTitle: translate('PhysicalRelease'),
|
||||||
|
releaseIcon: icons.DISC,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (digitalRelease && sortDate.isSame(moment(digitalRelease), 'day')) {
|
||||||
|
return {
|
||||||
|
eventDate: digitalRelease,
|
||||||
|
eventTitle: translate('DigitalRelease'),
|
||||||
|
releaseIcon: icons.MOVIE_FILE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inCinemas && sortDate.isSame(moment(inCinemas), 'day')) {
|
||||||
|
return {
|
||||||
|
eventDate: inCinemas,
|
||||||
|
eventTitle: translate('InCinemas'),
|
||||||
|
releaseIcon: icons.IN_CINEMAS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
eventDate: null,
|
||||||
|
eventTitle: null,
|
||||||
|
releaseIcon: null,
|
||||||
|
};
|
||||||
|
}, [inCinemas, digitalRelease, physicalRelease, sortDate]);
|
||||||
|
|
||||||
|
const downloading = !!(queueItem || grabbed);
|
||||||
|
const statusStyle = getStatusStyle(
|
||||||
|
hasFile,
|
||||||
|
downloading,
|
||||||
|
isMonitored,
|
||||||
|
isAvailable
|
||||||
|
);
|
||||||
|
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||||
|
const link = `/movie/${titleSlug}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.event}>
|
||||||
|
<Link className={styles.underlay} to={link} />
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.date}>
|
||||||
|
{showDate && eventDate
|
||||||
|
? moment(eventDate).format(longDateFormat)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.releaseIcon}>
|
||||||
|
{releaseIcon ? (
|
||||||
|
<Icon name={releaseIcon} kind={kinds.DEFAULT} title={eventTitle} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.eventWrapper,
|
||||||
|
styles[statusStyle],
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.movieTitle}>{title}</div>
|
||||||
|
|
||||||
|
{showMovieInformation ? (
|
||||||
|
<div className={styles.genres}>{joinedGenres}</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{queueItem ? (
|
||||||
|
<span className={styles.statusIcon}>
|
||||||
|
<CalendarEventQueueDetails {...queueItem} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!queueItem && grabbed ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('MovieIsDownloading')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showCutoffUnmetIcon && movieFile && movieFile.qualityCutoffNotMet ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.MOVIE_FILE}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={translate('QualityCutoffNotMet')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AgendaEvent;
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import AgendaEvent from './AgendaEvent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.options,
|
|
||||||
createMovieSelector(),
|
|
||||||
createMovieFileSelector(),
|
|
||||||
createQueueItemSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendarOptions, movie, movieFile, queueItem, uiSettings) => {
|
|
||||||
return {
|
|
||||||
movie,
|
|
||||||
movieFile,
|
|
||||||
queueItem,
|
|
||||||
...calendarOptions,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(AgendaEvent);
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import Alert from 'Components/Alert';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import AgendaConnector from './Agenda/AgendaConnector';
|
|
||||||
import * as calendarViews from './calendarViews';
|
|
||||||
import CalendarDaysConnector from './Day/CalendarDaysConnector';
|
|
||||||
import DaysOfWeekConnector from './Day/DaysOfWeekConnector';
|
|
||||||
import CalendarHeaderConnector from './Header/CalendarHeaderConnector';
|
|
||||||
import styles from './Calendar.css';
|
|
||||||
|
|
||||||
class Calendar extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
isPopulated,
|
|
||||||
error,
|
|
||||||
view
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.calendar}>
|
|
||||||
{
|
|
||||||
isFetching && !isPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!isFetching && !!error &&
|
|
||||||
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && view === calendarViews.AGENDA &&
|
|
||||||
<div className={styles.calendarContent}>
|
|
||||||
<CalendarHeaderConnector />
|
|
||||||
<AgendaConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!error && isPopulated && view !== calendarViews.AGENDA &&
|
|
||||||
<div className={styles.calendarContent}>
|
|
||||||
<CalendarHeaderConnector />
|
|
||||||
<DaysOfWeekConnector />
|
|
||||||
<CalendarDaysConnector />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Calendar.propTypes = {
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
|
||||||
error: PropTypes.object,
|
|
||||||
view: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Calendar;
|
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import useCurrentPage from 'Helpers/Hooks/useCurrentPage';
|
||||||
|
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||||
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import {
|
||||||
|
clearCalendar,
|
||||||
|
fetchCalendar,
|
||||||
|
gotoCalendarToday,
|
||||||
|
} from 'Store/Actions/calendarActions';
|
||||||
|
import {
|
||||||
|
clearMovieFiles,
|
||||||
|
fetchMovieFiles,
|
||||||
|
} from 'Store/Actions/movieFileActions';
|
||||||
|
import {
|
||||||
|
clearQueueDetails,
|
||||||
|
fetchQueueDetails,
|
||||||
|
} from 'Store/Actions/queueActions';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import {
|
||||||
|
registerPagePopulator,
|
||||||
|
unregisterPagePopulator,
|
||||||
|
} from 'Utilities/pagePopulator';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import Agenda from './Agenda/Agenda';
|
||||||
|
import CalendarDays from './Day/CalendarDays';
|
||||||
|
import DaysOfWeek from './Day/DaysOfWeek';
|
||||||
|
import CalendarHeader from './Header/CalendarHeader';
|
||||||
|
import styles from './Calendar.css';
|
||||||
|
|
||||||
|
const UPDATE_DELAY = 3600000; // 1 hour
|
||||||
|
|
||||||
|
function Calendar() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const requestCurrentPage = useCurrentPage();
|
||||||
|
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const { isFetching, isPopulated, error, items, time, view } = useSelector(
|
||||||
|
(state: AppState) => state.calendar
|
||||||
|
);
|
||||||
|
|
||||||
|
const isRefreshingMovie = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.REFRESH_MOVIE)
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstDayOfWeek = useSelector(
|
||||||
|
(state: AppState) => state.settings.ui.item.firstDayOfWeek
|
||||||
|
);
|
||||||
|
|
||||||
|
const wasRefreshingMovie = usePrevious(isRefreshingMovie);
|
||||||
|
const previousFirstDayOfWeek = usePrevious(firstDayOfWeek);
|
||||||
|
const previousItems = usePrevious(items);
|
||||||
|
|
||||||
|
const handleScheduleUpdate = useCallback(() => {
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
|
||||||
|
function updateCalendar() {
|
||||||
|
dispatch(gotoCalendarToday());
|
||||||
|
updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTimeout.current = setTimeout(updateCalendar, UPDATE_DELAY);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleScheduleUpdate();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
dispatch(clearCalendar());
|
||||||
|
dispatch(clearQueueDetails());
|
||||||
|
dispatch(clearMovieFiles());
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
};
|
||||||
|
}, [dispatch, handleScheduleUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (requestCurrentPage) {
|
||||||
|
dispatch(fetchCalendar());
|
||||||
|
} else {
|
||||||
|
dispatch(gotoCalendarToday());
|
||||||
|
}
|
||||||
|
}, [requestCurrentPage, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const repopulate = () => {
|
||||||
|
dispatch(fetchQueueDetails({ time, view }));
|
||||||
|
dispatch(fetchCalendar({ time, view }));
|
||||||
|
};
|
||||||
|
|
||||||
|
registerPagePopulator(repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unregisterPagePopulator(repopulate);
|
||||||
|
};
|
||||||
|
}, [time, view, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleScheduleUpdate();
|
||||||
|
}, [time, handleScheduleUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
previousFirstDayOfWeek != null &&
|
||||||
|
firstDayOfWeek !== previousFirstDayOfWeek
|
||||||
|
) {
|
||||||
|
dispatch(fetchCalendar({ time, view }));
|
||||||
|
}
|
||||||
|
}, [time, view, firstDayOfWeek, previousFirstDayOfWeek, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (wasRefreshingMovie && !isRefreshingMovie) {
|
||||||
|
dispatch(fetchCalendar({ time, view }));
|
||||||
|
}
|
||||||
|
}, [time, view, isRefreshingMovie, wasRefreshingMovie, dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!previousItems || hasDifferentItems(items, previousItems)) {
|
||||||
|
const movieIds = selectUniqueIds<Movie, number>(items, 'id');
|
||||||
|
const movieFileIds = selectUniqueIds<Movie, number>(items, 'movieFileId');
|
||||||
|
|
||||||
|
if (items.length) {
|
||||||
|
dispatch(fetchQueueDetails({ movieIds }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (movieFileIds.length) {
|
||||||
|
dispatch(fetchMovieFiles({ movieFileIds }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [items, previousItems, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.calendar}>
|
||||||
|
{isFetching && !isPopulated ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && error ? (
|
||||||
|
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!error && isPopulated && view === 'agenda' ? (
|
||||||
|
<div className={styles.calendarContent}>
|
||||||
|
<CalendarHeader />
|
||||||
|
<Agenda />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!error && isPopulated && view !== 'agenda' ? (
|
||||||
|
<div className={styles.calendarContent}>
|
||||||
|
<CalendarHeader />
|
||||||
|
<DaysOfWeek />
|
||||||
|
<CalendarDays />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Calendar;
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import * as calendarActions from 'Store/Actions/calendarActions';
|
|
||||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
|
||||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
|
||||||
import Calendar from './Calendar';
|
|
||||||
|
|
||||||
const UPDATE_DELAY = 3600000; // 1 hour
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
(state) => state.settings.ui.item.firstDayOfWeek,
|
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MOVIE),
|
|
||||||
(calendar, firstDayOfWeek, isRefreshingMovie) => {
|
|
||||||
return {
|
|
||||||
...calendar,
|
|
||||||
isRefreshingMovie,
|
|
||||||
firstDayOfWeek
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
...calendarActions,
|
|
||||||
fetchMovieFiles,
|
|
||||||
clearMovieFiles,
|
|
||||||
fetchQueueDetails,
|
|
||||||
clearQueueDetails
|
|
||||||
};
|
|
||||||
|
|
||||||
class CalendarConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const {
|
|
||||||
useCurrentPage,
|
|
||||||
fetchCalendar,
|
|
||||||
gotoCalendarToday
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
|
||||||
|
|
||||||
if (useCurrentPage) {
|
|
||||||
fetchCalendar();
|
|
||||||
} else {
|
|
||||||
gotoCalendarToday();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
items,
|
|
||||||
time,
|
|
||||||
view,
|
|
||||||
isRefreshingMovie,
|
|
||||||
firstDayOfWeek
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (hasDifferentItems(prevProps.items, items)) {
|
|
||||||
const movieFileIds = selectUniqueIds(items, 'movieFileId');
|
|
||||||
|
|
||||||
if (movieFileIds.length) {
|
|
||||||
this.props.fetchMovieFiles({ movieFileIds });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (items.length) {
|
|
||||||
this.props.fetchQueueDetails();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.time !== time) {
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.firstDayOfWeek !== firstDayOfWeek) {
|
|
||||||
this.props.fetchCalendar({ time, view });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevProps.isRefreshingMovie && !isRefreshingMovie) {
|
|
||||||
this.props.fetchCalendar({ time, view });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
unregisterPagePopulator(this.repopulate);
|
|
||||||
this.props.clearCalendar();
|
|
||||||
this.props.clearQueueDetails();
|
|
||||||
this.props.clearMovieFiles();
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
repopulate = () => {
|
|
||||||
const {
|
|
||||||
time,
|
|
||||||
view
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.fetchQueueDetails({ time, view });
|
|
||||||
this.props.fetchCalendar({ time, view });
|
|
||||||
};
|
|
||||||
|
|
||||||
scheduleUpdate = () => {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
|
|
||||||
this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY);
|
|
||||||
};
|
|
||||||
|
|
||||||
clearUpdateTimeout = () => {
|
|
||||||
if (this.updateTimeoutId) {
|
|
||||||
clearTimeout(this.updateTimeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
updateCalendar = () => {
|
|
||||||
this.props.gotoCalendarToday();
|
|
||||||
this.scheduleUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onCalendarViewChange = (view) => {
|
|
||||||
this.props.setCalendarView({ view });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTodayPress = () => {
|
|
||||||
this.props.gotoCalendarToday();
|
|
||||||
};
|
|
||||||
|
|
||||||
onPreviousPress = () => {
|
|
||||||
this.props.gotoCalendarPreviousRange();
|
|
||||||
};
|
|
||||||
|
|
||||||
onNextPress = () => {
|
|
||||||
this.props.gotoCalendarNextRange();
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Calendar
|
|
||||||
{...this.props}
|
|
||||||
onCalendarViewChange={this.onCalendarViewChange}
|
|
||||||
onTodayPress={this.onTodayPress}
|
|
||||||
onPreviousPress={this.onPreviousPress}
|
|
||||||
onNextPress={this.onNextPress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarConnector.propTypes = {
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
time: PropTypes.string,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
firstDayOfWeek: PropTypes.number.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isRefreshingMovie: PropTypes.bool.isRequired,
|
|
||||||
setCalendarView: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarToday: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarPreviousRange: PropTypes.func.isRequired,
|
|
||||||
gotoCalendarNextRange: PropTypes.func.isRequired,
|
|
||||||
clearCalendar: PropTypes.func.isRequired,
|
|
||||||
fetchCalendar: PropTypes.func.isRequired,
|
|
||||||
fetchMovieFiles: PropTypes.func.isRequired,
|
|
||||||
clearMovieFiles: PropTypes.func.isRequired,
|
|
||||||
fetchQueueDetails: PropTypes.func.isRequired,
|
|
||||||
clearQueueDetails: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarConnector);
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
|
||||||
import PageContent from 'Components/Page/PageContent';
|
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|
||||||
import { align, icons } from 'Helpers/Props';
|
|
||||||
import NoMovie from 'Movie/NoMovie';
|
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CalendarConnector from './CalendarConnector';
|
|
||||||
import CalendarFilterModal from './CalendarFilterModal';
|
|
||||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
|
||||||
import LegendConnector from './Legend/LegendConnector';
|
|
||||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
|
||||||
import styles from './CalendarPage.css';
|
|
||||||
|
|
||||||
const MINIMUM_DAY_WIDTH = 120;
|
|
||||||
|
|
||||||
class CalendarPage extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isCalendarLinkModalOpen: false,
|
|
||||||
isOptionsModalOpen: false,
|
|
||||||
width: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMeasure = ({ width }) => {
|
|
||||||
this.setState({ width });
|
|
||||||
const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH)));
|
|
||||||
|
|
||||||
this.props.onDaysCountChange(days);
|
|
||||||
};
|
|
||||||
|
|
||||||
onGetCalendarLinkPress = () => {
|
|
||||||
this.setState({ isCalendarLinkModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onGetCalendarLinkModalClose = () => {
|
|
||||||
this.setState({ isCalendarLinkModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onOptionsPress = () => {
|
|
||||||
this.setState({ isOptionsModalOpen: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
onOptionsModalClose = () => {
|
|
||||||
this.setState({ isOptionsModalOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onSearchMissingPress = () => {
|
|
||||||
const {
|
|
||||||
missingMovieIds,
|
|
||||||
onSearchMissingPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSearchMissingPress(missingMovieIds);
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
hasMovie,
|
|
||||||
movieError,
|
|
||||||
movieIsFetching,
|
|
||||||
movieIsPopulated,
|
|
||||||
missingMovieIds,
|
|
||||||
customFilters,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isSearchingForMissing,
|
|
||||||
useCurrentPage,
|
|
||||||
onRssSyncPress,
|
|
||||||
onFilterSelect
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isCalendarLinkModalOpen,
|
|
||||||
isOptionsModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const isMeasured = this.state.width > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<PageContent title={translate('Calendar')}>
|
|
||||||
<PageToolbar>
|
|
||||||
<PageToolbarSection>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('ICalLink')}
|
|
||||||
iconName={icons.CALENDAR}
|
|
||||||
onPress={this.onGetCalendarLinkPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarSeparator />
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('RssSync')}
|
|
||||||
iconName={icons.RSS}
|
|
||||||
isSpinning={isRssSyncExecuting}
|
|
||||||
onPress={onRssSyncPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('SearchForMissing')}
|
|
||||||
iconName={icons.SEARCH}
|
|
||||||
isDisabled={!missingMovieIds.length}
|
|
||||||
isSpinning={isSearchingForMissing}
|
|
||||||
onPress={this.onSearchMissingPress}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
|
||||||
<PageToolbarButton
|
|
||||||
label={translate('Options')}
|
|
||||||
iconName={icons.POSTER}
|
|
||||||
onPress={this.onOptionsPress}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FilterMenu
|
|
||||||
alignMenu={align.RIGHT}
|
|
||||||
isDisabled={!hasMovie}
|
|
||||||
selectedFilterKey={selectedFilterKey}
|
|
||||||
filters={filters}
|
|
||||||
customFilters={customFilters}
|
|
||||||
filterModalConnectorComponent={CalendarFilterModal}
|
|
||||||
onFilterSelect={onFilterSelect}
|
|
||||||
/>
|
|
||||||
</PageToolbarSection>
|
|
||||||
</PageToolbar>
|
|
||||||
|
|
||||||
<PageContentBody
|
|
||||||
className={styles.calendarPageBody}
|
|
||||||
innerClassName={styles.calendarInnerPageBody}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
movieIsFetching && !movieIsPopulated &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
movieError &&
|
|
||||||
<div className={styles.errorMessage}>
|
|
||||||
{getErrorMessage(movieError, 'Failed to load movies from API')}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!movieError && movieIsPopulated && hasMovie &&
|
|
||||||
<Measure
|
|
||||||
whitelist={['width']}
|
|
||||||
onMeasure={this.onMeasure}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
isMeasured ?
|
|
||||||
<CalendarConnector
|
|
||||||
useCurrentPage={useCurrentPage}
|
|
||||||
/> :
|
|
||||||
<div />
|
|
||||||
}
|
|
||||||
</Measure>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!movieError && movieIsPopulated && !hasMovie &&
|
|
||||||
<NoMovie totalItems={0} />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
hasMovie && !movieError &&
|
|
||||||
<LegendConnector />
|
|
||||||
}
|
|
||||||
</PageContentBody>
|
|
||||||
|
|
||||||
<CalendarLinkModal
|
|
||||||
isOpen={isCalendarLinkModalOpen}
|
|
||||||
onModalClose={this.onGetCalendarLinkModalClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarOptionsModal
|
|
||||||
isOpen={isOptionsModalOpen}
|
|
||||||
onModalClose={this.onOptionsModalClose}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarPage.propTypes = {
|
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
hasMovie: PropTypes.bool.isRequired,
|
|
||||||
movieError: PropTypes.object,
|
|
||||||
movieIsFetching: PropTypes.bool.isRequired,
|
|
||||||
movieIsPopulated: PropTypes.bool.isRequired,
|
|
||||||
missingMovieIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
|
||||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
|
||||||
isSearchingForMissing: PropTypes.bool.isRequired,
|
|
||||||
useCurrentPage: PropTypes.bool.isRequired,
|
|
||||||
onSearchMissingPress: PropTypes.func.isRequired,
|
|
||||||
onDaysCountChange: PropTypes.func.isRequired,
|
|
||||||
onRssSyncPress: PropTypes.func.isRequired,
|
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarPage;
|
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as commandNames from 'Commands/commandNames';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import useMeasure from 'Helpers/Hooks/useMeasure';
|
||||||
|
import { align, icons } from 'Helpers/Props';
|
||||||
|
import NoMovie from 'Movie/NoMovie';
|
||||||
|
import {
|
||||||
|
searchMissing,
|
||||||
|
setCalendarDaysCount,
|
||||||
|
setCalendarFilter,
|
||||||
|
} from 'Store/Actions/calendarActions';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
|
||||||
|
import { isCommandExecuting } from 'Utilities/Command';
|
||||||
|
import isBefore from 'Utilities/Date/isBefore';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import Calendar from './Calendar';
|
||||||
|
import CalendarFilterModal from './CalendarFilterModal';
|
||||||
|
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||||
|
import Legend from './Legend/Legend';
|
||||||
|
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||||
|
import styles from './CalendarPage.css';
|
||||||
|
|
||||||
|
const MINIMUM_DAY_WIDTH = 120;
|
||||||
|
|
||||||
|
function createMissingMovieIdsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.start,
|
||||||
|
(state: AppState) => state.calendar.end,
|
||||||
|
(state: AppState) => state.calendar.items,
|
||||||
|
(state: AppState) => state.queue.details.items,
|
||||||
|
(start, end, movies, queueDetails) => {
|
||||||
|
return movies.reduce<number[]>((acc, movie) => {
|
||||||
|
const { inCinemas } = movie;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!movie.movieFileId &&
|
||||||
|
inCinemas &&
|
||||||
|
moment(inCinemas).isAfter(start) &&
|
||||||
|
moment(inCinemas).isBefore(end) &&
|
||||||
|
isBefore(inCinemas) &&
|
||||||
|
!queueDetails.some(
|
||||||
|
(details) => !!details.movie && details.movie.id === movie.id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
acc.push(movie.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createIsSearchingSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.searchMissingCommandId,
|
||||||
|
createCommandsSelector(),
|
||||||
|
(searchMissingCommandId, commands) => {
|
||||||
|
if (searchMissingCommandId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isCommandExecuting(
|
||||||
|
commands.find((command) => {
|
||||||
|
return command.id === searchMissingCommandId;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarPage() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const { selectedFilterKey, filters } = useSelector(
|
||||||
|
(state: AppState) => state.calendar
|
||||||
|
);
|
||||||
|
const missingMovieIds = useSelector(createMissingMovieIdsSelector());
|
||||||
|
const isSearchingForMissing = useSelector(createIsSearchingSelector());
|
||||||
|
const isRssSyncExecuting = useSelector(
|
||||||
|
createCommandExecutingSelector(commandNames.RSS_SYNC)
|
||||||
|
);
|
||||||
|
const customFilters = useSelector(createCustomFiltersSelector('calendar'));
|
||||||
|
const hasMovies = !!useSelector(createMovieCountSelector());
|
||||||
|
|
||||||
|
const [pageContentRef, { width }] = useMeasure();
|
||||||
|
const [isCalendarLinkModalOpen, setIsCalendarLinkModalOpen] = useState(false);
|
||||||
|
const [isOptionsModalOpen, setIsOptionsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
const isMeasured = width > 0;
|
||||||
|
const PageComponent = hasMovies ? Calendar : NoMovie;
|
||||||
|
|
||||||
|
const handleGetCalendarLinkPress = useCallback(() => {
|
||||||
|
setIsCalendarLinkModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleGetCalendarLinkModalClose = useCallback(() => {
|
||||||
|
setIsCalendarLinkModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOptionsPress = useCallback(() => {
|
||||||
|
setIsOptionsModalOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleOptionsModalClose = useCallback(() => {
|
||||||
|
setIsOptionsModalOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleRssSyncPress = useCallback(() => {
|
||||||
|
dispatch(
|
||||||
|
executeCommand({
|
||||||
|
name: commandNames.RSS_SYNC,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const handleSearchMissingPress = useCallback(() => {
|
||||||
|
dispatch(searchMissing({ movieIds: missingMovieIds }));
|
||||||
|
}, [missingMovieIds, dispatch]);
|
||||||
|
|
||||||
|
const handleFilterSelect = useCallback(
|
||||||
|
(key: string | number) => {
|
||||||
|
dispatch(setCalendarFilter({ selectedFilterKey: key }));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (width === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayCount = Math.max(
|
||||||
|
3,
|
||||||
|
Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH))
|
||||||
|
);
|
||||||
|
|
||||||
|
dispatch(setCalendarDaysCount({ dayCount }));
|
||||||
|
}, [width, dispatch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title={translate('Calendar')}>
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('ICalLink')}
|
||||||
|
iconName={icons.CALENDAR}
|
||||||
|
onPress={handleGetCalendarLinkPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('RssSync')}
|
||||||
|
iconName={icons.RSS}
|
||||||
|
isSpinning={isRssSyncExecuting}
|
||||||
|
onPress={handleRssSyncPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('SearchForMissing')}
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!missingMovieIds.length}
|
||||||
|
isSpinning={isSearchingForMissing}
|
||||||
|
onPress={handleSearchMissingPress}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<PageToolbarButton
|
||||||
|
label={translate('Options')}
|
||||||
|
iconName={icons.POSTER}
|
||||||
|
onPress={handleOptionsPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
isDisabled={!hasMovies}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={CalendarFilterModal}
|
||||||
|
onFilterSelect={handleFilterSelect}
|
||||||
|
/>
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBody
|
||||||
|
ref={pageContentRef}
|
||||||
|
className={styles.calendarPageBody}
|
||||||
|
innerClassName={styles.calendarInnerPageBody}
|
||||||
|
>
|
||||||
|
{isMeasured ? <PageComponent totalItems={0} /> : <div />}
|
||||||
|
{hasMovies && <Legend />}
|
||||||
|
</PageContentBody>
|
||||||
|
|
||||||
|
<CalendarLinkModal
|
||||||
|
isOpen={isCalendarLinkModalOpen}
|
||||||
|
onModalClose={handleGetCalendarLinkModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CalendarOptionsModal
|
||||||
|
isOpen={isOptionsModalOpen}
|
||||||
|
onModalClose={handleOptionsModalClose}
|
||||||
|
/>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarPage;
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
|
||||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
|
||||||
import createMovieCountSelector from 'Store/Selectors/createMovieCountSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import { isCommandExecuting } from 'Utilities/Command';
|
|
||||||
import isBefore from 'Utilities/Date/isBefore';
|
|
||||||
import CalendarPage from './CalendarPage';
|
|
||||||
|
|
||||||
function createMissingMovieIdsSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.start,
|
|
||||||
(state) => state.calendar.end,
|
|
||||||
(state) => state.calendar.items,
|
|
||||||
(state) => state.queue.details.items,
|
|
||||||
(start, end, movies, queueDetails) => {
|
|
||||||
return movies.reduce((acc, movie) => {
|
|
||||||
const inCinemas = movie.inCinemas;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!movie.hasFile &&
|
|
||||||
moment(inCinemas).isAfter(start) &&
|
|
||||||
moment(inCinemas).isBefore(end) &&
|
|
||||||
isBefore(movie.inCinemas) &&
|
|
||||||
!queueDetails.some((details) => details.movieId === movie.id)
|
|
||||||
) {
|
|
||||||
acc.push(movie.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createIsSearchingSelector() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.searchMissingCommandId,
|
|
||||||
createCommandsSelector(),
|
|
||||||
(searchMissingCommandId, commands) => {
|
|
||||||
if (searchMissingCommandId == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return isCommandExecuting(commands.find((command) => {
|
|
||||||
return command.id === searchMissingCommandId;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.selectedFilterKey,
|
|
||||||
(state) => state.calendar.filters,
|
|
||||||
createCustomFiltersSelector('calendar'),
|
|
||||||
createMovieCountSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
createMissingMovieIdsSelector(),
|
|
||||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
|
||||||
createIsSearchingSelector(),
|
|
||||||
(
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
movieCount,
|
|
||||||
uiSettings,
|
|
||||||
missingMovieIds,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isSearchingForMissing
|
|
||||||
) => {
|
|
||||||
return {
|
|
||||||
selectedFilterKey,
|
|
||||||
filters,
|
|
||||||
customFilters,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
|
||||||
hasMovie: !!movieCount.count,
|
|
||||||
movieError: movieCount.error,
|
|
||||||
movieIsFetching: movieCount.isFetching,
|
|
||||||
movieIsPopulated: movieCount.isPopulated,
|
|
||||||
missingMovieIds,
|
|
||||||
isRssSyncExecuting,
|
|
||||||
isSearchingForMissing
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onRssSyncPress() {
|
|
||||||
dispatch(executeCommand({
|
|
||||||
name: commandNames.RSS_SYNC
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
onSearchMissingPress(movieIds) {
|
|
||||||
dispatch(searchMissing({ movieIds }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onDaysCountChange(dayCount) {
|
|
||||||
dispatch(setCalendarDaysCount({ dayCount }));
|
|
||||||
},
|
|
||||||
|
|
||||||
onFilterSelect(selectedFilterKey) {
|
|
||||||
dispatch(setCalendarFilter({ selectedFilterKey }));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withCurrentPage(
|
|
||||||
connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage)
|
|
||||||
);
|
|
||||||
@@ -1,23 +1,61 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
|
import CalendarEvent from 'Calendar/Events/CalendarEvent';
|
||||||
import CalendarEvent from 'typings/CalendarEvent';
|
import { CalendarEvent as CalendarEventModel } from 'typings/Calendar';
|
||||||
import styles from './CalendarDay.css';
|
import styles from './CalendarDay.css';
|
||||||
|
|
||||||
|
function sort(items: CalendarEventModel[]) {
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
const aDate = moment(a.inCinemas).unix();
|
||||||
|
const bDate = moment(b.inCinemas).unix();
|
||||||
|
|
||||||
|
return aDate - bDate;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCalendarEventsConnector(date: string) {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.calendar.items,
|
||||||
|
(state: AppState) => state.calendar.options,
|
||||||
|
(items, options) => {
|
||||||
|
const { showCinemaRelease, showDigitalRelease, showPhysicalRelease } =
|
||||||
|
options;
|
||||||
|
const momentDate = moment(date);
|
||||||
|
|
||||||
|
const filtered = items.filter(
|
||||||
|
({ inCinemas, digitalRelease, physicalRelease }) => {
|
||||||
|
return (
|
||||||
|
(showCinemaRelease &&
|
||||||
|
inCinemas &&
|
||||||
|
momentDate.isSame(moment(inCinemas), 'day')) ||
|
||||||
|
(showDigitalRelease &&
|
||||||
|
digitalRelease &&
|
||||||
|
momentDate.isSame(moment(digitalRelease), 'day')) ||
|
||||||
|
(showPhysicalRelease &&
|
||||||
|
physicalRelease &&
|
||||||
|
momentDate.isSame(moment(physicalRelease), 'day'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return sort(filtered);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface CalendarDayProps {
|
interface CalendarDayProps {
|
||||||
date: string;
|
date: string;
|
||||||
time: string;
|
|
||||||
isTodaysDate: boolean;
|
isTodaysDate: boolean;
|
||||||
events: CalendarEvent[];
|
|
||||||
view: string;
|
|
||||||
onEventModalOpenToggle(...args: unknown[]): unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CalendarDay(props: CalendarDayProps) {
|
function CalendarDay({ date, isTodaysDate }: CalendarDayProps) {
|
||||||
const { date, time, isTodaysDate, events, view, onEventModalOpenToggle } =
|
const { time, view } = useSelector((state: AppState) => state.calendar);
|
||||||
props;
|
const events = useSelector(createCalendarEventsConnector(date));
|
||||||
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -50,13 +88,7 @@ function CalendarDay(props: CalendarDayProps) {
|
|||||||
<div>
|
<div>
|
||||||
{events.map((event) => {
|
{events.map((event) => {
|
||||||
return (
|
return (
|
||||||
<CalendarEventConnector
|
<CalendarEvent key={event.id} {...event} date={date as string} />
|
||||||
key={event.id}
|
|
||||||
{...event}
|
|
||||||
movieId={event.id}
|
|
||||||
date={date as string}
|
|
||||||
onEventModalOpenToggle={onEventModalOpenToggle}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import CalendarDay from './CalendarDay';
|
|
||||||
|
|
||||||
function sort(items) {
|
|
||||||
return _.sortBy(items, (item) => {
|
|
||||||
if (item.isGroup) {
|
|
||||||
return moment(item.events[0].inCinemas).unix();
|
|
||||||
}
|
|
||||||
|
|
||||||
return moment(item.inCinemas).unix();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createCalendarEventsConnector() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { date }) => date,
|
|
||||||
(state) => state.calendar.items,
|
|
||||||
(date, items) => {
|
|
||||||
const filtered = _.filter(items, (item) => {
|
|
||||||
return (item.inCinemas && moment(date).isSame(moment(item.inCinemas), 'day')) ||
|
|
||||||
(item.physicalRelease && moment(date).isSame(moment(item.physicalRelease), 'day')) ||
|
|
||||||
(item.digitalRelease && moment(date).isSame(moment(item.digitalRelease), 'day'));
|
|
||||||
});
|
|
||||||
|
|
||||||
return sort(filtered);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
createCalendarEventsConnector(),
|
|
||||||
(calendar, events) => {
|
|
||||||
return {
|
|
||||||
time: calendar.time,
|
|
||||||
view: calendar.view,
|
|
||||||
events
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class CalendarDayConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<CalendarDay
|
|
||||||
{...this.props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarDayConnector.propTypes = {
|
|
||||||
date: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(CalendarDayConnector);
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import isToday from 'Utilities/Date/isToday';
|
|
||||||
import CalendarDayConnector from './CalendarDayConnector';
|
|
||||||
import styles from './CalendarDays.css';
|
|
||||||
|
|
||||||
class CalendarDays extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this._touchStart = null;
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
todaysDate: moment().startOf('day').toISOString(),
|
|
||||||
isEventModalOpen: false
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const view = this.props.view;
|
|
||||||
|
|
||||||
if (view === calendarViews.MONTH) {
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('touchstart', this.onTouchStart);
|
|
||||||
window.addEventListener('touchend', this.onTouchEnd);
|
|
||||||
window.addEventListener('touchcancel', this.onTouchCancel);
|
|
||||||
window.addEventListener('touchmove', this.onTouchMove);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
|
|
||||||
window.removeEventListener('touchstart', this.onTouchStart);
|
|
||||||
window.removeEventListener('touchend', this.onTouchEnd);
|
|
||||||
window.removeEventListener('touchcancel', this.onTouchCancel);
|
|
||||||
window.removeEventListener('touchmove', this.onTouchMove);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
scheduleUpdate = () => {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
const todaysDate = moment().startOf('day');
|
|
||||||
const diff = moment().diff(todaysDate.clone().add(1, 'day'));
|
|
||||||
|
|
||||||
this.setState({ todaysDate: todaysDate.toISOString() });
|
|
||||||
|
|
||||||
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
|
|
||||||
};
|
|
||||||
|
|
||||||
clearUpdateTimeout = () => {
|
|
||||||
if (this.updateTimeoutId) {
|
|
||||||
clearTimeout(this.updateTimeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onEventModalOpenToggle = (isEventModalOpen) => {
|
|
||||||
this.setState({ isEventModalOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
onTouchStart = (event) => {
|
|
||||||
const touches = event.touches;
|
|
||||||
const touchStart = touches[0].pageX;
|
|
||||||
|
|
||||||
if (touches.length !== 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
touchStart < 50 ||
|
|
||||||
this.props.isSidebarVisible ||
|
|
||||||
this.state.isEventModalOpen
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._touchStart = touchStart;
|
|
||||||
};
|
|
||||||
|
|
||||||
onTouchEnd = (event) => {
|
|
||||||
const touches = event.changedTouches;
|
|
||||||
const currentTouch = touches[0].pageX;
|
|
||||||
|
|
||||||
if (!this._touchStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentTouch > this._touchStart && currentTouch - this._touchStart > 100) {
|
|
||||||
this.props.onNavigatePrevious();
|
|
||||||
} else if (currentTouch < this._touchStart && this._touchStart - currentTouch > 100) {
|
|
||||||
this.props.onNavigateNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._touchStart = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
onTouchCancel = (event) => {
|
|
||||||
this._touchStart = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
onTouchMove = (event) => {
|
|
||||||
if (!this._touchStart) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dates,
|
|
||||||
view
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
styles.days,
|
|
||||||
styles[view]
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
dates.map((date) => {
|
|
||||||
return (
|
|
||||||
<CalendarDayConnector
|
|
||||||
key={date}
|
|
||||||
date={date}
|
|
||||||
isTodaysDate={isToday(date)}
|
|
||||||
onEventModalOpenToggle={this.onEventModalOpenToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarDays.propTypes = {
|
|
||||||
dates: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
isSidebarVisible: PropTypes.bool.isRequired,
|
|
||||||
onNavigatePrevious: PropTypes.func.isRequired,
|
|
||||||
onNavigateNext: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarDays;
|
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
|
import {
|
||||||
|
gotoCalendarNextRange,
|
||||||
|
gotoCalendarPreviousRange,
|
||||||
|
} from 'Store/Actions/calendarActions';
|
||||||
|
import CalendarDay from './CalendarDay';
|
||||||
|
import styles from './CalendarDays.css';
|
||||||
|
|
||||||
|
function CalendarDays() {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const { dates, view } = useSelector((state: AppState) => state.calendar);
|
||||||
|
const isSidebarVisible = useSelector(
|
||||||
|
(state: AppState) => state.app.isSidebarVisible
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const touchStart = useRef<number | null>(null);
|
||||||
|
const [todaysDate, setTodaysDate] = useState(
|
||||||
|
moment().startOf('day').toISOString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const scheduleUpdate = useCallback(() => {
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
|
||||||
|
const todaysDate = moment().startOf('day');
|
||||||
|
const diff = moment().diff(todaysDate.clone().add(1, 'day'));
|
||||||
|
|
||||||
|
setTodaysDate(todaysDate.toISOString());
|
||||||
|
|
||||||
|
updateTimeout.current = setTimeout(scheduleUpdate, diff);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchStart = useCallback(
|
||||||
|
(event: TouchEvent) => {
|
||||||
|
const touches = event.touches;
|
||||||
|
const currentTouch = touches[0].pageX;
|
||||||
|
|
||||||
|
if (touches.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTouch < 50 || isSidebarVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
touchStart.current = currentTouch;
|
||||||
|
},
|
||||||
|
[isSidebarVisible]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTouchEnd = useCallback(
|
||||||
|
(event: TouchEvent) => {
|
||||||
|
const touches = event.changedTouches;
|
||||||
|
const currentTouch = touches[0].pageX;
|
||||||
|
|
||||||
|
if (!touchStart.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentTouch > touchStart.current &&
|
||||||
|
currentTouch - touchStart.current > 100
|
||||||
|
) {
|
||||||
|
dispatch(gotoCalendarPreviousRange());
|
||||||
|
} else if (
|
||||||
|
currentTouch < touchStart.current &&
|
||||||
|
touchStart.current - currentTouch > 100
|
||||||
|
) {
|
||||||
|
dispatch(gotoCalendarNextRange());
|
||||||
|
}
|
||||||
|
|
||||||
|
touchStart.current = null;
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTouchCancel = useCallback(() => {
|
||||||
|
touchStart.current = null;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTouchMove = useCallback(() => {
|
||||||
|
if (!touchStart.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (view === calendarViews.MONTH) {
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
}, [view, scheduleUpdate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('touchstart', handleTouchStart);
|
||||||
|
window.addEventListener('touchend', handleTouchEnd);
|
||||||
|
window.addEventListener('touchcancel', handleTouchCancel);
|
||||||
|
window.addEventListener('touchmove', handleTouchMove);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('touchstart', handleTouchStart);
|
||||||
|
window.removeEventListener('touchend', handleTouchEnd);
|
||||||
|
window.removeEventListener('touchcancel', handleTouchCancel);
|
||||||
|
window.removeEventListener('touchmove', handleTouchMove);
|
||||||
|
};
|
||||||
|
}, [handleTouchStart, handleTouchEnd, handleTouchCancel, handleTouchMove]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(styles.days, styles[view as keyof typeof styles])}
|
||||||
|
>
|
||||||
|
{dates.map((date) => {
|
||||||
|
return (
|
||||||
|
<CalendarDay
|
||||||
|
key={date}
|
||||||
|
date={date}
|
||||||
|
isTodaysDate={date === todaysDate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarDays;
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { gotoCalendarNextRange, gotoCalendarPreviousRange } from 'Store/Actions/calendarActions';
|
|
||||||
import CalendarDays from './CalendarDays';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
(state) => state.app.isSidebarVisible,
|
|
||||||
(calendar, isSidebarVisible) => {
|
|
||||||
return {
|
|
||||||
dates: calendar.dates,
|
|
||||||
view: calendar.view,
|
|
||||||
isSidebarVisible
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
onNavigatePrevious: gotoCalendarPreviousRange,
|
|
||||||
onNavigateNext: gotoCalendarNextRange
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarDays);
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
|
||||||
import styles from './DayOfWeek.css';
|
|
||||||
|
|
||||||
class DayOfWeek extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
date,
|
|
||||||
view,
|
|
||||||
isTodaysDate,
|
|
||||||
calendarWeekColumnHeader,
|
|
||||||
shortDateFormat,
|
|
||||||
showRelativeDates
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const highlightToday = view !== calendarViews.MONTH && isTodaysDate;
|
|
||||||
const momentDate = moment(date);
|
|
||||||
let formatedDate = momentDate.format('dddd');
|
|
||||||
|
|
||||||
if (view === calendarViews.WEEK) {
|
|
||||||
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
|
||||||
} else if (view === calendarViews.FORECAST) {
|
|
||||||
formatedDate = getRelativeDate({ date, shortDateFormat, showRelativeDates });
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
styles.dayOfWeek,
|
|
||||||
view === calendarViews.DAY && styles.isSingleDay,
|
|
||||||
highlightToday && styles.isToday
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatedDate}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DayOfWeek.propTypes = {
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
isTodaysDate: PropTypes.bool.isRequired,
|
|
||||||
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DayOfWeek;
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React from 'react';
|
||||||
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import styles from './DayOfWeek.css';
|
||||||
|
|
||||||
|
interface DayOfWeekProps {
|
||||||
|
date: string;
|
||||||
|
view: string;
|
||||||
|
isTodaysDate: boolean;
|
||||||
|
calendarWeekColumnHeader: string;
|
||||||
|
shortDateFormat: string;
|
||||||
|
showRelativeDates: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DayOfWeek(props: DayOfWeekProps) {
|
||||||
|
const {
|
||||||
|
date,
|
||||||
|
view,
|
||||||
|
isTodaysDate,
|
||||||
|
calendarWeekColumnHeader,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const highlightToday = view !== calendarViews.MONTH && isTodaysDate;
|
||||||
|
const momentDate = moment(date);
|
||||||
|
let formatedDate = momentDate.format('dddd');
|
||||||
|
|
||||||
|
if (view === calendarViews.WEEK) {
|
||||||
|
formatedDate = momentDate.format(calendarWeekColumnHeader);
|
||||||
|
} else if (view === calendarViews.FORECAST) {
|
||||||
|
formatedDate = getRelativeDate({
|
||||||
|
date,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.dayOfWeek,
|
||||||
|
view === calendarViews.DAY && styles.isSingleDay,
|
||||||
|
highlightToday && styles.isToday
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{formatedDate}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DayOfWeek;
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import DayOfWeek from './DayOfWeek';
|
|
||||||
import styles from './DaysOfWeek.css';
|
|
||||||
|
|
||||||
class DaysOfWeek extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
todaysDate: moment().startOf('day').toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
this.updateTimeoutId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
const view = this.props.view;
|
|
||||||
|
|
||||||
if (view !== calendarViews.AGENDA || view !== calendarViews.MONTH) {
|
|
||||||
this.scheduleUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
scheduleUpdate = () => {
|
|
||||||
this.clearUpdateTimeout();
|
|
||||||
const todaysDate = moment().startOf('day');
|
|
||||||
const diff = todaysDate.clone().add(1, 'day').diff(moment());
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
todaysDate: todaysDate.toISOString()
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
|
|
||||||
};
|
|
||||||
|
|
||||||
clearUpdateTimeout = () => {
|
|
||||||
if (this.updateTimeoutId) {
|
|
||||||
clearTimeout(this.updateTimeoutId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
dates,
|
|
||||||
view,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (view === calendarViews.AGENDA) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.daysOfWeek}>
|
|
||||||
{
|
|
||||||
dates.map((date) => {
|
|
||||||
return (
|
|
||||||
<DayOfWeek
|
|
||||||
key={date}
|
|
||||||
date={date}
|
|
||||||
view={view}
|
|
||||||
isTodaysDate={date === this.state.todaysDate}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DaysOfWeek.propTypes = {
|
|
||||||
dates: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
view: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DaysOfWeek;
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import moment from 'moment';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import DayOfWeek from './DayOfWeek';
|
||||||
|
import styles from './DaysOfWeek.css';
|
||||||
|
|
||||||
|
function DaysOfWeek() {
|
||||||
|
const { dates, view } = useSelector((state: AppState) => state.calendar);
|
||||||
|
const { calendarWeekColumnHeader, shortDateFormat, showRelativeDates } =
|
||||||
|
useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const updateTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const [todaysDate, setTodaysDate] = useState(
|
||||||
|
moment().startOf('day').toISOString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const scheduleUpdate = useCallback(() => {
|
||||||
|
clearTimeout(updateTimeout.current);
|
||||||
|
|
||||||
|
const todaysDate = moment().startOf('day');
|
||||||
|
const diff = moment().diff(todaysDate.clone().add(1, 'day'));
|
||||||
|
|
||||||
|
setTodaysDate(todaysDate.toISOString());
|
||||||
|
|
||||||
|
updateTimeout.current = setTimeout(scheduleUpdate, diff);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (view !== calendarViews.AGENDA && view !== calendarViews.MONTH) {
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
}, [view, scheduleUpdate]);
|
||||||
|
|
||||||
|
if (view === calendarViews.AGENDA) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.daysOfWeek}>
|
||||||
|
{dates.map((date) => {
|
||||||
|
return (
|
||||||
|
<DayOfWeek
|
||||||
|
key={date}
|
||||||
|
date={date}
|
||||||
|
view={view}
|
||||||
|
isTodaysDate={date === todaysDate}
|
||||||
|
calendarWeekColumnHeader={calendarWeekColumnHeader}
|
||||||
|
shortDateFormat={shortDateFormat}
|
||||||
|
showRelativeDates={showRelativeDates}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DaysOfWeek;
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import DaysOfWeek from './DaysOfWeek';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar,
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendar, UiSettings) => {
|
|
||||||
return {
|
|
||||||
dates: calendar.dates.slice(0, 7),
|
|
||||||
view: calendar.view,
|
|
||||||
calendarWeekColumnHeader: UiSettings.calendarWeekColumnHeader,
|
|
||||||
shortDateFormat: UiSettings.shortDateFormat,
|
|
||||||
showRelativeDates: UiSettings.showRelativeDates
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(DaysOfWeek);
|
|
||||||
@@ -34,7 +34,8 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
|
|||||||
}
|
}
|
||||||
|
|
||||||
.movieTitle,
|
.movieTitle,
|
||||||
.genres {
|
.genres,
|
||||||
|
.eventType {
|
||||||
@add-mixin truncate;
|
@add-mixin truncate;
|
||||||
flex: 1 0 1px;
|
flex: 1 0 1px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ interface CssExports {
|
|||||||
'continuing': string;
|
'continuing': string;
|
||||||
'downloaded': string;
|
'downloaded': string;
|
||||||
'event': string;
|
'event': string;
|
||||||
|
'eventType': string;
|
||||||
'genres': string;
|
'genres': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'missingMonitored': string;
|
'missingMonitored': string;
|
||||||
|
|||||||
@@ -1,177 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
|
||||||
import styles from './CalendarEvent.css';
|
|
||||||
|
|
||||||
class CalendarEvent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
movieFile,
|
|
||||||
isAvailable,
|
|
||||||
inCinemas,
|
|
||||||
physicalRelease,
|
|
||||||
digitalRelease,
|
|
||||||
title,
|
|
||||||
titleSlug,
|
|
||||||
genres,
|
|
||||||
date,
|
|
||||||
monitored,
|
|
||||||
certification,
|
|
||||||
hasFile,
|
|
||||||
grabbed,
|
|
||||||
queueItem,
|
|
||||||
showMovieInformation,
|
|
||||||
showCutoffUnmetIcon,
|
|
||||||
fullColorEvents,
|
|
||||||
colorImpairedMode
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const isDownloading = !!(queueItem || grabbed);
|
|
||||||
const isMonitored = monitored;
|
|
||||||
const statusStyle = getStatusStyle(hasFile, isDownloading, isMonitored, isAvailable);
|
|
||||||
const joinedGenres = genres.slice(0, 2).join(', ');
|
|
||||||
const link = `/movie/${titleSlug}`;
|
|
||||||
const eventType = [];
|
|
||||||
|
|
||||||
if (inCinemas && moment(date).isSame(moment(inCinemas), 'day')) {
|
|
||||||
eventType.push('Cinemas');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (physicalRelease && moment(date).isSame(moment(physicalRelease), 'day')) {
|
|
||||||
eventType.push('Physical');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (digitalRelease && moment(date).isSame(moment(digitalRelease), 'day')) {
|
|
||||||
eventType.push('Digital');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.event,
|
|
||||||
styles[statusStyle],
|
|
||||||
colorImpairedMode && 'colorImpaired',
|
|
||||||
fullColorEvents && 'fullColor'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
className={styles.underlay}
|
|
||||||
to={link}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className={styles.overlay} >
|
|
||||||
<div className={styles.info}>
|
|
||||||
<div className={styles.movieTitle}>
|
|
||||||
{title}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
styles.statusContainer,
|
|
||||||
fullColorEvents && 'fullColor'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
queueItem ?
|
|
||||||
<span className={styles.statusIcon}>
|
|
||||||
<CalendarEventQueueDetails
|
|
||||||
{...queueItem}
|
|
||||||
fullColorEvents={fullColorEvents}
|
|
||||||
/>
|
|
||||||
</span> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!queueItem && grabbed ?
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
title={translate('MovieIsDownloading')}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showCutoffUnmetIcon &&
|
|
||||||
!!movieFile &&
|
|
||||||
movieFile.qualityCutoffNotMet ?
|
|
||||||
<Icon
|
|
||||||
className={styles.statusIcon}
|
|
||||||
name={icons.MOVIE_FILE}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('QualityCutoffNotMet')}
|
|
||||||
/> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
showMovieInformation ?
|
|
||||||
<div className={styles.movieInfo}>
|
|
||||||
<div className={styles.genres}>
|
|
||||||
{joinedGenres}
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
showMovieInformation ?
|
|
||||||
<div className={styles.movieInfo}>
|
|
||||||
<div className={styles.genres}>
|
|
||||||
{eventType.join(', ')}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{certification}
|
|
||||||
</div>
|
|
||||||
</div> :
|
|
||||||
null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarEvent.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
movieFile: PropTypes.object,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
titleSlug: PropTypes.string.isRequired,
|
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
|
||||||
inCinemas: PropTypes.string,
|
|
||||||
physicalRelease: PropTypes.string,
|
|
||||||
digitalRelease: PropTypes.string,
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
monitored: PropTypes.bool.isRequired,
|
|
||||||
certification: PropTypes.string,
|
|
||||||
hasFile: PropTypes.bool.isRequired,
|
|
||||||
grabbed: PropTypes.bool,
|
|
||||||
queueItem: PropTypes.object,
|
|
||||||
// These props come from the connector, not marked as required to appease TS for now.
|
|
||||||
showMovieInformation: PropTypes.bool,
|
|
||||||
showCutoffUnmetIcon: PropTypes.bool,
|
|
||||||
fullColorEvents: PropTypes.bool,
|
|
||||||
timeFormat: PropTypes.string,
|
|
||||||
colorImpairedMode: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
CalendarEvent.defaultProps = {
|
|
||||||
genres: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarEvent;
|
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import useMovieFile from 'MovieFile/useMovieFile';
|
||||||
|
import { createQueueItemSelectorForHook } from 'Store/Selectors/createQueueItemSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
||||||
|
import styles from './CalendarEvent.css';
|
||||||
|
|
||||||
|
interface CalendarEventProps {
|
||||||
|
id: number;
|
||||||
|
movieFileId?: number;
|
||||||
|
title: string;
|
||||||
|
titleSlug: string;
|
||||||
|
genres: string[];
|
||||||
|
certification?: string;
|
||||||
|
date: string;
|
||||||
|
inCinemas?: string;
|
||||||
|
digitalRelease?: string;
|
||||||
|
physicalRelease?: string;
|
||||||
|
isAvailable: boolean;
|
||||||
|
monitored: boolean;
|
||||||
|
hasFile: boolean;
|
||||||
|
grabbed?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarEvent({
|
||||||
|
id,
|
||||||
|
movieFileId,
|
||||||
|
title,
|
||||||
|
titleSlug,
|
||||||
|
genres = [],
|
||||||
|
certification,
|
||||||
|
date,
|
||||||
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
|
physicalRelease,
|
||||||
|
isAvailable,
|
||||||
|
monitored: isMonitored,
|
||||||
|
hasFile,
|
||||||
|
grabbed,
|
||||||
|
}: CalendarEventProps) {
|
||||||
|
const movieFile = useMovieFile(movieFileId);
|
||||||
|
const queueItem = useSelector(createQueueItemSelectorForHook(id));
|
||||||
|
|
||||||
|
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
|
||||||
|
|
||||||
|
const {
|
||||||
|
showMovieInformation,
|
||||||
|
showCinemaRelease,
|
||||||
|
showDigitalRelease,
|
||||||
|
showPhysicalRelease,
|
||||||
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
|
} = useSelector((state: AppState) => state.calendar.options);
|
||||||
|
|
||||||
|
const isDownloading = !!(queueItem || grabbed);
|
||||||
|
const statusStyle = getStatusStyle(
|
||||||
|
hasFile,
|
||||||
|
isDownloading,
|
||||||
|
isMonitored,
|
||||||
|
isAvailable
|
||||||
|
);
|
||||||
|
const joinedGenres = genres.slice(0, 2).join(', ');
|
||||||
|
const link = `/movie/${titleSlug}`;
|
||||||
|
|
||||||
|
const eventTypes = useMemo(() => {
|
||||||
|
const momentDate = moment(date);
|
||||||
|
|
||||||
|
const types = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
showCinemaRelease &&
|
||||||
|
inCinemas &&
|
||||||
|
momentDate.isSame(moment(inCinemas), 'day')
|
||||||
|
) {
|
||||||
|
types.push('Cinemas');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
showDigitalRelease &&
|
||||||
|
digitalRelease &&
|
||||||
|
momentDate.isSame(moment(digitalRelease), 'day')
|
||||||
|
) {
|
||||||
|
types.push('Digital');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
showPhysicalRelease &&
|
||||||
|
physicalRelease &&
|
||||||
|
momentDate.isSame(moment(physicalRelease), 'day')
|
||||||
|
) {
|
||||||
|
types.push('Physical');
|
||||||
|
}
|
||||||
|
|
||||||
|
return types;
|
||||||
|
}, [
|
||||||
|
date,
|
||||||
|
showCinemaRelease,
|
||||||
|
showDigitalRelease,
|
||||||
|
showPhysicalRelease,
|
||||||
|
inCinemas,
|
||||||
|
digitalRelease,
|
||||||
|
physicalRelease,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.event,
|
||||||
|
styles[statusStyle],
|
||||||
|
enableColorImpairedMode && 'colorImpaired',
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link className={styles.underlay} to={link} />
|
||||||
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.movieTitle}>{title}</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.statusContainer,
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{queueItem ? (
|
||||||
|
<span className={styles.statusIcon}>
|
||||||
|
<CalendarEventQueueDetails {...queueItem} />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{!queueItem && grabbed ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.DOWNLOADING}
|
||||||
|
title={translate('MovieIsDownloading')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{showCutoffUnmetIcon &&
|
||||||
|
!!movieFile &&
|
||||||
|
movieFile.qualityCutoffNotMet ? (
|
||||||
|
<Icon
|
||||||
|
className={styles.statusIcon}
|
||||||
|
name={icons.MOVIE_FILE}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title={translate('QualityCutoffNotMet')}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showMovieInformation ? (
|
||||||
|
<>
|
||||||
|
<div className={styles.movieInfo}>
|
||||||
|
<div className={styles.genres}>{joinedGenres}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.movieInfo}>
|
||||||
|
<div className={styles.eventType}>{eventTypes.join(', ')}</div>
|
||||||
|
|
||||||
|
<div>{certification}</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarEvent;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
|
||||||
import createQueueItemSelector from 'Store/Selectors/createQueueItemSelector';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import CalendarEvent from './CalendarEvent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.calendar.options,
|
|
||||||
createMovieSelector(),
|
|
||||||
createQueueItemSelector(),
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(calendarOptions, movie, queueItem, uiSettings) => {
|
|
||||||
return {
|
|
||||||
movie,
|
|
||||||
queueItem,
|
|
||||||
...calendarOptions,
|
|
||||||
timeFormat: uiSettings.timeFormat,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps)(CalendarEvent);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user