1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

163 Commits

Author SHA1 Message Date
bogdan 4c1b3a7b82 Bump MailKit and Microsoft.Data.SqlClient 2025-09-07 10:45:27 -05:00
Mark McDowall 6396d83fa7 Change authentication to Forms if set to Basic
(cherry picked from commit dfb6fdfbeb7ce85b287b41fed80f2511727353e5)
2025-09-07 10:44:48 -05:00
Bogdan bd203d841a Fixed: Validation for tags label 2025-09-07 10:44:48 -05:00
Bogdan e96d7580f4 Fixed: Removed support for movie file tokens in Movie Folder Format 2025-09-07 10:44:48 -05:00
Bogdan eb0f7c62b6 New: Validation for movie file tokens in Movie Folder Format 2025-09-07 10:44:48 -05:00
Mark McDowall 41fa0de230 New: Remove Basic Auth
(cherry picked from commit 0f9e063e2146812f6e963363eee70a524612f354)
2025-09-07 10:44:48 -05:00
bakerboy448 cdc6a6dd27 New: Default wanted language for quality profiles changed to Original 2025-09-07 10:44:48 -05:00
Bogdan 1891ac1536 Bump Swashbuckle to 8.1.4 2025-09-07 10:44:48 -05:00
Bogdan 2539d46f7c Bump version to 6.0.0 2025-09-07 10:44:43 -05:00
Bogdan 32fe345144 New: Support removed for linux-x86 2025-09-07 10:44:20 -05:00
Bogdan 9d2193636e New: Migrate appdata folder for .NET 8 on OSX 2025-09-07 10:44:20 -05:00
Bogdan 1e898c2647 New: Bump to .NET 8
Co-authored-by: Qstick <qstick@gmail.com>
2025-09-07 10:44:20 -05:00
bakerboy448 a00ee08750 Bump to 5.28.1 2025-09-07 00:31:28 -05:00
Michon van Dooren 54cbbe05d9 New: (NFO Metadata) Include the TMDB Collection ID (#11164) 2025-09-06 05:58:07 -05:00
Mark McDowall 57f602eb02 New: Changing icon during import to blue 2025-09-03 17:02:22 -05:00
bakerboy448 e841c9b764 Bump to 5.28.0 2025-09-03 07:07:57 -05:00
bakerboy448 81bbaf8946 Fixed: Add missing translation keys 2025-09-01 11:29:59 -05:00
bakerboy448 8b4288fa18 New: UI Note that Filters are for movie properties only
Co-authored-by: PearsonFlyer <john@theediguy.com>

Closes #11200
2025-09-01 11:29:59 -05:00
Weblate 9aa3061e8e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Xoores <servarr-35466@xoores.cz>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-08-30 12:09:23 -05:00
Weblate 308c58f729 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ReDFiRe <wwsoft@abv.bg>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: rtme <pps.kmg@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2025-08-21 21:39:34 +01:00
grapexy d38492188a New: Georgian language support (#11209) 2025-08-17 22:11:49 -05:00
bakerboy448 50e75e1362 Bump to 5.27.5 2025-08-17 14:44:46 -04:00
bakerboy448 f36845c251 Fixed: Parse UHDBDRip as BluRay quality 2025-08-16 10:35:25 -05:00
bakerboy448 110a338fb6 Fixed: TMDb List Paging (#11201) 2025-08-16 09:20:34 -05:00
Mark McDowall 3fcbaf9259 New: Move auth success logging to debug
Closes #7978

(cherry picked from commit 9ebe043bd94d036fe2ab45f3bb3f882cda48e211)
2025-08-12 12:20:36 -05:00
Luigi 576eff1890 New: Select with poster click in movie selection (#11187) 2025-08-12 11:49:58 -05:00
jkhsjdhjs b0284bda07 Fixed: Parse HDDVDRip as BluRay 2025-08-11 18:53:36 -05:00
Bwaffles c78666009d New: Add Year sorting to Discover page 2025-08-11 18:52:56 -05:00
Mark McDowall b51d1beaaa Don't log debug messages for API key validation
(cherry picked from commit 78ca30d1f81361a2dabaddd0036b764859b858af)
2025-08-11 18:32:42 -05:00
Weblate 4d22bf1ceb Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ArLab1 <arnaud.laberge@hotmail.com>
Co-authored-by: Ethan Francois <ethanfrancois0@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: adri1m64 <adrien.melle@laposte.net>
Co-authored-by: deamok <deamok@gmail.com>
Co-authored-by: dtorner <dtorner@gmail.com>
Co-authored-by: scdani <csonkadancsi@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2025-08-11 18:31:56 -05:00
bakerboy448 f9562b9b76 Bump version to 5.27.4 2025-08-03 01:10:22 -05:00
bakerboy448 6851c26328 Bump SixLabors.ImageSharp to 3.1.11 2025-07-31 21:52:59 -05:00
bakerboy448 e29be26fc9 Fixed: Prevent using Original names with other movie file tokens (#11175)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2025-07-25 08:16:18 -05:00
bakerboy448 f6bd2f52d5 Bump version to 5.27.3 2025-07-23 08:51:28 -05:00
Denis Gheorghescu 8bef9b4da7 New:(Pushcut) Improved Notification Details (#10897) 2025-07-19 10:43:18 -04:00
Mark McDowall 787c387036 Return error if Manual Import called without items
(cherry picked from commit 4bdb0408f1bafa38b777a41babb1a775f99a94c1)
2025-07-09 16:19:57 -05:00
bakerboy448 0525256115 Bump version to 5.27.2 2025-07-08 18:37:58 -05:00
bakerboy448 5767e181b7 New: Improve Reject for Unknown Movie Messaging (#11063) 2025-07-08 18:25:51 -05:00
Mark McDowall 1cf3ef5dff New: Improve stored UI settings for multiple instances under the same host
Closes #10671
Fixes #11146

(cherry picked from commit 6677fd11168de6dbf78d03bfedf67b89dfe1df53)
2025-07-08 18:07:52 -05:00
Weblate b6bad2398c Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: ACHN SYPS <achn.syps@gmail.com>
Co-authored-by: EdiTurn <yyxstter@gmail.com>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: josef <josef.holzapfel@proton.me>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2025-07-08 18:06:46 -05:00
nuxen 16308e4b1c Fixed: xvid not always detected correctly (#11138) 2025-07-07 20:00:13 -05:00
bakerboy448 bd7465fae4 Fixed: Allow Discover Exclusions of Movies without Year (Year 0)
Fixes #11135
2025-06-28 09:07:26 -05:00
Weblate c0d70485c3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: HanaO00 <lwin24452@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-06-19 18:20:22 -05:00
Bogdan c743383912 Fixed: Deleting tags from UI
Fixes #11131
2025-06-16 20:34:00 +03:00
Servarr d93c1d7808 Automated API Docs update 2025-06-16 20:22:31 +03:00
nuxen 0e2e7e4259 New: Support for multiple movieIds in Rename API endpoint 2025-06-16 19:09:19 +03:00
Bogdan e6b27512c9 Bump version to 5.27.1 2025-06-15 09:20:29 +03:00
Bogdan dae5e86b2c Fixed: Skip title searches for Newznab/Torznab indexers when movie year is missing
Prevents useless text searches of `Movie Title 0` when year is missing.

Fixes #10569
2025-06-14 13:20:11 +03:00
Bogdan 71f032d175 Bump Polly to 8.6.0 2025-06-11 23:13:54 +03:00
Bogdan 5a6db29dbd Bump version to 5.27.0 2025-06-11 23:11:20 +03:00
Weblate 2dac2dd35b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Qqqqqquexx <946921515@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2025-06-11 15:01:35 +03:00
Stevie Robinson b829638a77 Fixed: Include network drive types in Disk Space
(cherry picked from commit 9ffcd141a515e99604881a4ef383dadafef31eeb)
2025-06-10 15:31:34 +03:00
Mark McDowall b6b7f13839 Prevent should refresh movie metadata from failing
Fixed: Prevent error checking if movie metadata should be refreshed from failing refresh movies task
(cherry picked from commit 3eed84c67938fed308e562e69cf7bcd727063803)
2025-06-10 15:31:34 +03:00
Mark McDowall a9ad197b75 New: Update wording when removing a root folder
(cherry picked from commit 51c17fd3122f7b96a4155593d465ba32870d0c91)
2025-06-10 15:31:34 +03:00
Mark McDowall 1b28116a7e Fixed: Escape backticks in discord notifications
(cherry picked from commit 70c74fc1769f2094a14faaa103c49d744534be9f)
2025-06-10 15:31:34 +03:00
Bogdan 5870c88e1c Fix fullscreen automation screenshots 2025-06-09 22:05:09 +03:00
Servarr 0629832bd0 Automated API Docs update 2025-06-09 15:22:52 +03:00
Bogdan 430897c710 Fixed: Hide separators when page toolbar shows all buttons on small screens
Fixes #11124
2025-06-09 15:11:45 +03:00
Bogdan 9c42246eef Bump SixLabors.ImageSharp to 3.1.9 2025-06-08 11:32:36 +03:00
Weblate 489a86b253 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_TW/
Translation: Servarr/Radarr
2025-06-08 10:42:02 +03:00
Robert Dailey 9c8d3b679d Add 'qualitydefinition/limits' endpoint to get size limitations
(cherry picked from commit 24f03fc1e96eba215f96312c791cf167f10499c7)
2025-06-08 10:41:37 +03:00
Bogdan b2e51d1613 Bump version to 5.26.2 2025-06-08 10:30:06 +03:00
Michael Peleshenko a95b1f2992 Fixed: Handling movies with empty IMDB IDs in lists clean library 2025-06-07 11:35:44 +03:00
Mark McDowall ac33b15048 Convert Tags to TypeScript
(cherry picked from commit 60529f0bacf2398838ef8d7843490a35046a1093)
2025-06-04 22:16:24 +03:00
Bogdan d28f03af28 Fixed: Allow more prefixes and suffixes for Release Year naming token 2025-06-04 19:50:08 +03:00
Bogdan 73b99d0be2 Add translation for missing movies count from collection 2025-06-04 18:54:09 +03:00
Stevie Robinson 15c34a61de New: Ability to clone Import Lists
(cherry picked from commit 2314d0b506e30d3a965497a052bc5e54fa0beb81)

Closes #10948
2025-06-04 18:34:13 +03:00
Mark McDowall b99c536306 Convert ImportLists to TypeScript
(cherry picked from commit 10e3a237ef972540abcf4348bb56973d7ee19bd7)
2025-06-04 18:28:50 +03:00
Mark McDowall 2ebf391f85 Convert Media Management settings to TypeScript
(cherry picked from commit 27f81117ed188712600d8daf3ccb5121f9808458)
2025-06-04 17:50:00 +03:00
Mark McDowall 3945a2eeb8 Convert Indexer settings to TypeScript
(cherry picked from commit 6e008a8e855e67bb14b0e04bdb9042eebcacb59f)
2025-06-04 15:57:46 +03:00
Mark McDowall e6980df590 Convert SettingsToolbar to TypeScript
(cherry picked from commit fd09ca6e719a96f760006ed0f08756faa20b6f75)
2025-06-04 14:43:30 +03:00
nuxen 187dd79b9c Fixed: Allow opening curly bracket as prefix in naming format 2025-06-03 16:27:31 +03:00
Bogdan 22ef334de6 Fix translation token for root folders load error 2025-06-03 15:22:38 +03:00
Weblate c9eb9b8b98 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Tur3Q <andrejturan@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: cyrille <oscarboehringer@gmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translation: Servarr/Radarr
2025-06-03 14:31:18 +03:00
Bogdan 9c74c40fc6 Fixed: Quality sliders on some browsers
Fixes #11109
2025-06-01 18:07:25 +03:00
Mark McDowall 8911cbe872 Sync react-slider props for Quality sliders with upstream
(cherry picked from commit 9dab2ba6e4316879e4db8db47363476a5c4f13b2)
2025-06-01 17:54:31 +03:00
Ghworg 7e541d4653 Fixed: Display media info bitrates in bits (#11087) 2025-06-01 14:50:53 +03:00
Bogdan 1cc2237ac0 Bump version to 5.26.1 2025-06-01 10:39:43 +03:00
Bogdan 470963921d Fixed: Improve edition naming for ordinals and certain keywords
Co-authored-by: Brandon Shelley <brandon@pacificaviator.co>
2025-05-31 14:46:43 +03:00
Servarr 36f9ec4ea7 Automated API Docs update 2025-05-30 16:20:50 +03:00
Bogdan 9df2368601 New: Release type options for Calendar page 2025-05-30 15:39:00 +03:00
Bogdan e7d76350ec New: Release type options for Calendar Feed 2025-05-30 15:39:00 +03:00
Bogdan fd3828ff5d Update translation tokens for release dates on poster options 2025-05-30 01:04:29 +03:00
Bogdan 368e1fead8 Fixed: ISO language code for Bengali 2025-05-29 18:50:21 +03:00
Mark McDowall 5b357faf16 useMeasure instead of Measure in TypeScript components
(cherry picked from commit ee1a0a1f7175839c63595bef6d0221d3787189f4)
2025-05-29 17:14:47 +03:00
Bogdan 3f35b7c782 Convert Calendar to TypeScript
(cherry picked from commit 811eb36c7b1a5124270ff93d18d16944e654de81)

Co-authored-by: Mark McDowall <mark@mcdowall.ca>

Closes #10764
Closes #10776
Closes #10781
2025-05-29 17:14:07 +03:00
Bogdan 7d29deb93c Fixed: Parsing quality for release titles with Hybrid WEB 2025-05-28 12:54:00 +03:00
Mark McDowall d0bfdce9c5 Fixed: Sorting of some titles with acronyms or common words at the start
(cherry picked from commit 79436149eb6869033d2263cd9558dbe75b1d3a68)
2025-05-27 18:23:08 +03:00
Bogdan 5d0cd78667 Bump version to 5.26.0 2025-05-27 00:58:58 +03:00
Bogdan afbe0ebcd4 Fixed: Validation for remote path mapping
Fixes #11092
2025-05-26 21:26:07 +03:00
Bogdan bfbb7532a2 Bump version to 5.25.0 2025-05-26 13:25:58 +03:00
Bogdan c92d8c08f1 Follow redirects for usenet grabs on non-prod builds 2025-05-25 20:05:18 +03:00
bakerboy448 358ce92f85 Fixed: Production builds erroneously marked as not Production
build number exceeded; bump to 20k
fixes #11089
2025-05-25 11:48:29 -05:00
Servarr 3ec5a4b78a Automated API Docs update 2025-05-25 16:43:04 +03:00
Bogdan cb59ce891a New: Keywords custom filter and autotagging for movies 2025-05-25 14:55:45 +03:00
Weblate 4d3d46d796 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Alan <1790727569@qq.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: warkurre86 <tom.novo.86@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2025-05-25 14:12:23 +03:00
Bogdan 0941e51d27 Bump version to 5.24.1 2025-05-25 14:11:22 +03:00
Bogdan ff393a3f65 Show movie titles when poster is missing on collections page 2025-05-24 00:26:48 +03:00
Aden Northcote f5faf52469 Fixed: Update AutoTags on movie add (#11079)
Co-authored-by: aden <aden@hughorse.net>
2025-05-23 18:56:02 +03:00
Bogdan b5b4d4b971 Return error with missing field for movie files endpoint
Fixes #10555
2025-05-23 18:50:36 +03:00
Bogdan 873299701b Use UTC dates for TMDB Popular lists 2025-05-23 18:12:54 +03:00
Bogdan d14cca30d7 Use the thrown exception in http timeout handling
(cherry picked from commit 14e324ee30694ae017a39fd6f66392dc2d104617)
2025-05-23 12:25:47 +03:00
Stevie Robinson 5af61b5900 New: Ignore volumes containing .timemachine from Disk Space
(cherry picked from commit a853c537db0a6bd499a2277987dc170d2a1f5645)
2025-05-23 12:24:50 +03:00
carrossos a10759c7e9 Treat HTTP 410 response for failed download similarly to HTTP 404
(cherry picked from commit 818ae02a7a8f0a8ea0a44e0015e2667d96453332)
2025-05-23 12:24:37 +03:00
Stevie Robinson ac2d92007e New: Don't allow remote path to start with space
(cherry picked from commit 5ba3ff598770fdf9e5a53d490c8bcbdd6a59c4cc)
2025-05-23 12:13:27 +03:00
Mark McDowall 09cfdc3fa2 Increase maximum backup restoration size to 5GB
(cherry picked from commit e38deb34221ebf131adcce9551774898f46b1f7f)
2025-05-23 12:13:12 +03:00
Stevie Robinson 04f26dbff7 Ensure Custom Format Maximum Size won't overflow
(cherry picked from commit a50d2562649bbe77d0feb9fbfc594d56952e0a5e)
2025-05-23 12:12:37 +03:00
Bogdan 159f5df8cc Fix jump to character for Collections and Discover
Fix for a regression introduced in react-virtualized 9.21.2 when WindowScroller is used with Grids
2025-05-22 15:40:29 +03:00
Elerir b823ad8e65 New: Add Mongolian language
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2025-05-21 12:08:15 +03:00
Bogdan cc8bffc272 Bump version to 5.24.0 2025-05-17 15:56:03 +03:00
Bogdan e0b93a03fd Remove create_test_cases.py 2025-05-17 01:17:00 +03:00
Mark McDowall f7f5837d49 Convert Missing to TypeScript
(cherry picked from commit 3035521b93ef54a6cc6193a526be862976228669)
2025-05-16 19:38:18 +03:00
Mark McDowall c3ee8b3c90 Convert Cutoff Unmet to TypeScript
(cherry picked from commit 45c53bea865447aa543242e64e3d796c93117975)
2025-05-16 19:31:49 +03:00
Weblate 4de78e3bab Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_Hans/
Translation: Servarr/Radarr
2025-05-15 22:42:37 +03:00
Bogdan 426538c8af Remove console statement 2025-05-15 14:56:25 +03:00
Bogdan c82404c75b Fixed: Loading suggestions for header search input 2025-05-15 14:48:40 +03:00
Bogdan 9bee9841c1 Fixed: (PTP) Download torrent files with API credentials 2025-05-15 00:58:38 +03:00
Bogdan 010959d915 Bump @babel/runtime 2025-05-14 18:48:34 +03:00
Bogdan a600728916 Bump react-virtualized to 9.22.6
Bump @types/react
2025-05-14 18:37:10 +03:00
Bogdan bbfb8c7cc2 Bump babel, fontawesome icons, fuse.js, react-lazyload, react-use-measure and react-window 2025-05-14 14:37:12 +03:00
Bogdan 32418ea521 Bump core-js to 3.42 2025-05-14 14:37:12 +03:00
Bogdan 2c5c99e9b7 New: Deprecate use of movie file tokens in Movie Folder Format 2025-05-13 14:41:32 +03:00
Bogdan a5e5a63e45 Fixed: Upgrade notification title for Apprise 2025-05-13 00:43:46 +03:00
Bogdan 31b44d2c2e New: Include movie poster for Apprise 2025-05-12 22:54:42 +03:00
Bogdan da8e8a12de New: Include year in interactive searches title
Fixes #11070
2025-05-12 22:50:32 +03:00
v3DJG6GL 6506c97ce1 Fixed: Map SwissGerman to German (#11068)
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2025-05-12 16:50:38 +03:00
v3DJG6GL 5303a1992c New: Add Romansh language 2025-05-11 13:36:26 +03:00
Bogdan 042308c319 Bump version to 5.23.3 2025-05-11 10:53:03 +03:00
Bogdan 2e97e09f44 Fail build on missing test results
Ignore missing test results failure on FreeBSD
2025-05-10 13:46:35 +03:00
Bogdan ccfb9c0dad Bump SixLabors.ImageSharp to 3.1.8 2025-05-10 13:43:52 +03:00
Weblate b655d97e9e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: Robi Korb <robi.korb@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translation: Servarr/Radarr
2025-05-04 21:05:34 +03:00
Bogdan 3afcb91db6 Bump version to 5.23.2 2025-05-04 21:04:29 +03:00
Bogdan 704e2d6176 Fixed: (PTP) Sorting releases by time added 2025-05-01 19:22:00 +03:00
Mark McDowall 8314c37b1d Improve messaging when NZB contains invalid XML
(cherry picked from commit 728df146ada115a367bf1ce808482a4625e6098d)
2025-04-29 11:36:09 +03:00
Bogdan c2c3dfe917 Avoid varying logging message template between calls 2025-04-29 11:35:57 +03:00
Bogdan c58a9b3f2c Pass messages with arguments to NLog in LoggerExtensions
(cherry picked from commit 9683b0af35220bb0af801779a06d73feaeba809a)
2025-04-29 11:32:12 +03:00
Bogdan 65a532a7fd Fixed: Sidebar flickering on mobile 2025-04-28 11:56:16 +03:00
Bogdan 704d920dab Remove unused preload.js 2025-04-27 21:34:21 +03:00
Bogdan 025cb0788f Update default log level message 2025-04-27 21:10:35 +03:00
Mark McDowall 82c21d8bb1 Convert Log FIles to TypeScript
(cherry picked from commit 95929dd9c2b4460ec58b63136d64bd584e7dd263)
2025-04-27 21:08:26 +03:00
Mark McDowall 96f973c961 Convert Spinner button components to TypeScript
(cherry picked from commit a1d4bb53997748ef348af50dd967bae8e23e234d)
2025-04-27 20:42:38 +03:00
Mark McDowall a1ed440945 Convert Messages to TypeScript
(cherry picked from commit 0fdeb0566305311dcec344074b5de9e38b8d8090)
2025-04-27 20:38:21 +03:00
Mark McDowall 8caa839d99 Convert Table to TypeScript
(cherry picked from commit 699120a8fd54be9e70fb9a83298f94c8cb6a80bb)
2025-04-27 20:29:10 +03:00
Bogdan 9228e5dea0 Convert ImportListList component to TypeScript 2025-04-27 19:45:03 +03:00
Mark McDowall 371ac0921d Convert TagList components to TypeScript
(cherry picked from commit 20e1a8d116cf60d619f9525cc82867483d38df84)
2025-04-27 19:45:03 +03:00
Mark McDowall 937557e214 Convert Page components to TypeScript
(cherry picked from commit f35a27449d253260ba9c9fae28909cec8a87b4fe)
2025-04-27 19:45:03 +03:00
Mark McDowall 7fdaf41325 useMeasure instead of Measure in TypeScript components
(cherry picked from commit ee1a0a1f7175839c63595bef6d0221d3787189f4)
2025-04-27 19:45:03 +03:00
Bogdan 577eb4f4ca Bump version to 5.23.1 2025-04-27 11:47:54 +03:00
Weblate 311f41b306 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Gallyam Biktashev <gallyamb@gmail.com>
Co-authored-by: Hugoren Martinako <aumpfbahn@gmail.com>
Co-authored-by: Weblate <noreply-mt-weblate@weblate.org>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: zefir6 <zefir@mj12.pl>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2025-04-26 12:22:27 +03:00
Mark McDowall 78f3b1f403 Convert Menu components to TypeScript
(cherry picked from commit 12a1ef038753cdab89ae7137d06e1ba3810166b1)
2025-04-25 18:10:02 +03:00
Bogdan 4dc02dcb80 Bump core-js to 3.41 2025-04-24 17:01:45 +03:00
Bogdan 2f649e413d Bump caniuse db 2025-04-24 16:57:21 +03:00
Bogdan 107ddd3826 Fix maximum typo and clean unused CSS files 2025-04-24 16:53:36 +03:00
Bogdan dfdd2cba99 Page titles for collections and discover 2025-04-24 16:16:23 +03:00
Bogdan c57d68c3dd Remove unused register page populator 2025-04-24 15:21:20 +03:00
Bogdan 6cc02b734e Fixed: Refresh collections to clear stale state on bulk movies removal 2025-04-24 15:08:31 +03:00
Bogdan c5fa09dd86 Fixed: Restore scroll position for collections and discover on go back 2025-04-24 15:08:31 +03:00
Bogdan 29d59315b2 Bump version to 5.23.0 2025-04-23 22:04:25 +03:00
Bogdan 981a3c2db3 Fixed: Selecting No Change for quality profile inputs 2025-04-23 20:21:58 +03:00
Bogdan 3f2ea56bf9 Clear collection changes on add movie modal close 2025-04-23 11:33:40 +03:00
Servarr 1679ed1327 Automated API Docs update 2025-04-20 14:15:50 +03:00
Bogdan 69a1c1b21b Custom format scoring is not usable on movies index 2025-04-20 12:57:34 +03:00
Bogdan 5bd51832a0 Bump version to 5.22.4 2025-04-20 08:59:55 +03:00
657 changed files with 16618 additions and 18604 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet
{
"name": "Radarr",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-6.0",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-8.0",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"nodeGypDependencies": true,
+3 -6
View File
@@ -165,15 +165,12 @@ Thumbs.db
/tools/Addins/*
packages.config.md5sum
# Common IntelliJ Platform excludes
# Ignore Rider projects completely for now
.idea/
# ignore node_modules symlink
node_modules
node_modules.nosync
# API doc generation
.config/
# Ignore Jetbrains IntelliJ Workspace Directories
.idea/
+1 -1
View File
@@ -10,7 +10,7 @@
"request": "launch",
"preLaunchTask": "build dotnet",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/_output/net6.0/Radarr",
"program": "${workspaceFolder}/_output/net8.0/Radarr",
"args": [],
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
+52 -76
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.22.3'
majorVersion: '6.0.0'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.427'
dotnetVersion: '8.0.405'
nodeVersion: '20.X'
innoVersion: '6.2.2'
windowsImage: 'windows-2022'
@@ -106,7 +106,7 @@ stages:
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
fi
displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-extra-platforms
@@ -122,27 +122,23 @@ stages:
artifact: '$(osName)Backend'
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
- publish: '$(testsFolder)/net8.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
- publish: '$(testsFolder)/net8.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
- publish: '$(testsFolder)/net8.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
- publish: '$(testsFolder)/net8.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
- publish: '$(testsFolder)/net8.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
@@ -189,7 +185,7 @@ stages:
artifact: '$(osName)Frontend'
displayName: Publish Frontend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Installer
dependsOn:
- Build_Backend
@@ -260,21 +256,21 @@ stages:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/win-x64/net8.0
- task: ArchiveFiles@2
displayName: Create win-x86 zip
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
rootFolderOrFile: $(artifactsFolder)/win-x86/net8.0
- task: ArchiveFiles@2
displayName: Create osx-x64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net8.0
- task: ArchiveFiles@2
displayName: Create osx-x64 tar
inputs:
@@ -282,14 +278,14 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-x64/net8.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 app
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net8.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 tar
inputs:
@@ -297,7 +293,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-x64 tar
inputs:
@@ -305,7 +301,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-x64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-musl-x64 tar
inputs:
@@ -313,15 +309,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x86 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs:
@@ -329,7 +317,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-arm/net8.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm tar
inputs:
@@ -337,7 +325,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net8.0
- task: ArchiveFiles@2
displayName: Create linux-arm64 tar
inputs:
@@ -345,7 +333,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net8.0
- task: ArchiveFiles@2
displayName: Create linux-musl-arm64 tar
inputs:
@@ -353,7 +341,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net8.0
- task: ArchiveFiles@2
displayName: Create freebsd-x64 tar
inputs:
@@ -361,7 +349,7 @@ stages:
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net8.0
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'Packages'
displayName: Publish Packages
@@ -392,7 +380,7 @@ stages:
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
SENTRY_ORG: $(sentryOrg)
SENTRY_URL: $(sentryUrl)
- stage: Unit_Test
displayName: Unit Tests
dependsOn: Build_Backend
@@ -481,6 +469,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: ne(variables['testName'], 'freebsd-x64')
- job: Unit_Docker
displayName: Unit Docker
@@ -492,29 +481,19 @@ stages:
testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool:
vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ]
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -540,7 +519,8 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
- job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare
@@ -557,7 +537,7 @@ stages:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -596,6 +576,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres14 Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
- job: Unit_LinuxCore_Postgres15
displayName: Unit Native LinuxCore with Postgres15 Database
@@ -608,12 +589,12 @@ stages:
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -652,6 +633,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
- stage: Integration
displayName: Integration
@@ -695,7 +677,7 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -717,7 +699,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -734,6 +716,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres14
@@ -771,7 +754,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -796,6 +779,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
@@ -834,7 +818,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -859,6 +843,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
- job: Integration_FreeBSD
@@ -905,6 +890,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'FreeBSD Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: false
displayName: Publish Test Results
- job: Integration_Docker
@@ -918,29 +904,18 @@ stages:
artifactName: linux-musl-x64-tests
containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Radarr.*.linux-core-x86.tar.gz'
pool:
vmImage: ${{ variables.linuxImage }}
container: $[ variables['containerImage'] ]
timeoutInMinutes: 15
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
inputs:
version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
@@ -957,7 +932,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -974,12 +949,13 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Integration Tests'
failTaskOnFailedTests: true
failTaskOnMissingResultsFile: true
displayName: Publish Test Results
- stage: Automation
displayName: Automation
dependsOn: Packages
jobs:
- job: Automation
strategy:
@@ -1005,7 +981,7 @@ stages:
pool:
vmImage: $(imageName)
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
@@ -1027,7 +1003,7 @@ stages:
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
@@ -1055,6 +1031,7 @@ stages:
testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(osName) Automation Tests'
failTaskOnFailedTests: $(failBuild)
failTaskOnMissingResultsFile: $(failBuild)
displayName: Publish Test Results
- stage: Analyze
@@ -1151,7 +1128,7 @@ stages:
- checkout: self
submodules: true
persistCredentials: true
fetchDepth: 1
fetchDepth: 1
- bash: ./docs.sh Windows
displayName: Create openapi.json
- bash: |
@@ -1220,13 +1197,13 @@ stages:
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: |
./build.sh --backend -f net6.0 -r win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
./build.sh --backend -f net8.0 -r win-x64
TEST_DIR=_tests/net8.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@3
condition: eq(variables['System.PullRequest.IsFork'], 'False')
displayName: Publish SonarCloud Results
- task: reportgenerator@5.3.11
- task: reportgenerator@5
displayName: Generate Coverage Report
inputs:
reports: '$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml'
@@ -1264,4 +1241,3 @@ stages:
DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+25 -27
View File
@@ -33,14 +33,14 @@ EnableExtraPlatformsInSDK()
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' "$BUNDLEDVERSIONS"
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
fi
}
@@ -79,9 +79,9 @@ Build()
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
else
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
dotnet msbuild -restore $slnFile -p:SelfContained=True -p:Configuration=Release -p:Platform=$platform -p:RuntimeIdentifiers=$RID -t:PublishAllRids
fi
ProgressEnd 'Build'
@@ -137,7 +137,7 @@ PackageLinux()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net8.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -165,7 +165,7 @@ PackageMacOS()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net8.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -377,15 +377,14 @@ then
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
PackageTests "net6.0" "win-x64"
PackageTests "net6.0" "win-x86"
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
PackageTests "net8.0" "win-x64"
PackageTests "net8.0" "win-x86"
PackageTests "net8.0" "linux-x64"
PackageTests "net8.0" "linux-musl-x64"
PackageTests "net8.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
PackageTests "net8.0" "freebsd-x64"
fi
else
PackageTests "$FRAMEWORK" "$RID"
@@ -413,20 +412,19 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
Package "net6.0" "win-x64"
Package "net6.0" "win-x86"
Package "net6.0" "linux-x64"
Package "net6.0" "linux-musl-x64"
Package "net6.0" "linux-arm64"
Package "net6.0" "linux-musl-arm64"
Package "net6.0" "linux-arm"
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
Package "net8.0" "win-x64"
Package "net8.0" "win-x86"
Package "net8.0" "linux-x64"
Package "net8.0" "linux-musl-x64"
Package "net8.0" "linux-arm64"
Package "net8.0" "linux-musl-arm64"
Package "net8.0" "linux-arm"
Package "net8.0" "linux-musl-arm"
Package "net8.0" "osx-x64"
Package "net8.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
Package "net8.0" "freebsd-x64"
fi
else
Package "$FRAMEWORK" "$RID"
@@ -436,7 +434,7 @@ fi
if [ "$INSTALLER" = "YES" ];
then
InstallInno
BuildInstaller "net6.0" "win-x64"
BuildInstaller "net6.0" "win-x86"
BuildInstaller "net8.0" "win-x64"
BuildInstaller "net8.0" "win-x86"
RemoveInno
fi
-44
View File
@@ -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
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/bash
set -e
FRAMEWORK="net6.0"
FRAMEWORK="net8.0"
PLATFORM=$1
ARCHITECTURE="${2:-x64}"
@@ -38,7 +38,7 @@ dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.6.2 Swashbuckle.AspNetCore.Cli
dotnet tool install --version 8.1.4 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/$FRAMEWORK/$RUNTIME/$application" v3 &
+1 -12
View File
@@ -14,7 +14,6 @@ module.exports = (env) => {
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = !!env.production;
const isProfiling = isProduction && !!env.profile;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
@@ -160,16 +159,6 @@ module.exports = (env) => {
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
}
}
},
{
test: [/\.jsx?$/, /\.tsx?$/],
exclude: /(node_modules|JsLibraries)/,
@@ -187,7 +176,7 @@ module.exports = (env) => {
loose: true,
debug: false,
useBuiltIns: 'entry',
corejs: '3.39'
corejs: '3.42'
}
]
]
@@ -145,7 +145,7 @@ function Blocklist() {
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
(selectedFilterKey: string | number) => {
dispatch(setBlocklistFilter({ selectedFilterKey }));
},
[dispatch]
@@ -26,7 +26,7 @@ function BlocklistDetailsModal(props: BlocklistDetailsModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>Details</ModalHeader>
<ModalHeader>{translate('Details')}</ModalHeader>
<ModalBody>
<DescriptionList>
+1 -1
View File
@@ -77,7 +77,7 @@ function History() {
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
(selectedFilterKey: string | number) => {
dispatch(setHistoryFilter({ selectedFilterKey }));
},
[dispatch]
+2 -2
View File
@@ -183,7 +183,7 @@ function Queue() {
});
const handleFilterSelect = useCallback(
(selectedFilterKey: string) => {
(selectedFilterKey: string | number) => {
dispatch(setQueueFilter({ selectedFilterKey }));
},
[dispatch]
@@ -304,7 +304,7 @@ function Queue() {
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Refresh"
label={translate('Refresh')}
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={handleRefreshPress}
+3 -3
View File
@@ -1,5 +1,5 @@
import React from 'react';
import Icon, { IconProps } from 'Components/Icon';
import Icon, { IconKind } from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds } from 'Helpers/Props';
import { TooltipPosition } from 'Helpers/Props/tooltipPositions';
@@ -61,7 +61,7 @@ function QueueStatus(props: QueueStatusProps) {
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind: IconProps['kind'] = kinds.DEFAULT;
let iconKind: IconKind = kinds.DEFAULT;
let title = translate('Downloading');
if (status === 'paused') {
@@ -90,7 +90,7 @@ function QueueStatus(props: QueueStatusProps) {
if (trackedDownloadState === 'importing') {
title += ` - ${translate('Importing')}`;
iconKind = kinds.PURPLE;
iconKind = kinds.PRIMARY;
}
if (trackedDownloadState === 'failedPending') {
@@ -108,7 +108,7 @@ class ImportMovie extends Component {
{
!rootFoldersFetching && !!rootFoldersError ?
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
{translate('RootFoldersLoadError')}
</Alert> :
null
}
@@ -93,7 +93,7 @@ class ImportMovieSelectFolder extends Component {
{
!isFetching && error ?
<Alert kind={kinds.DANGER}>
{translate('UnableToLoadRootFolders')}
{translate('RootFoldersLoadError')}
</Alert> :
null
}
+3 -3
View File
@@ -4,7 +4,7 @@ import React from 'react';
import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import PageConnector from 'Components/Page/PageConnector';
import Page from 'Components/Page/Page';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes';
@@ -22,9 +22,9 @@ function App({ store, history }: AppProps) {
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme />
<PageConnector>
<Page>
<AppRoutes />
</PageConnector>
</Page>
</ConnectedRouter>
</Provider>
</QueryClientProvider>
+12 -18
View File
@@ -5,7 +5,7 @@ import History from 'Activity/History/History';
import Queue from 'Activity/Queue/Queue';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import CalendarPage from 'Calendar/CalendarPage';
import CollectionConnector from 'Collection/CollectionConnector';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
@@ -15,9 +15,9 @@ 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 ImportListSettings from 'Settings/ImportLists/ImportListSettings';
import IndexerSettings from 'Settings/Indexers/IndexerSettings';
import MediaManagement from 'Settings/MediaManagement/MediaManagement';
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles';
@@ -32,8 +32,8 @@ 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';
import CutoffUnmet from 'Wanted/CutoffUnmet/CutoffUnmet';
import Missing from 'Wanted/Missing/Missing';
function RedirectWithUrlBase() {
return <Redirect to={getPathWithUrlBase('/')} />;
@@ -73,7 +73,7 @@ function AppRoutes() {
Calendar
*/}
<Route path="/calendar" component={CalendarPageConnector} />
<Route path="/calendar" component={CalendarPage} />
{/*
Activity
@@ -89,9 +89,9 @@ function AppRoutes() {
Wanted
*/}
<Route path="/wanted/missing" component={MissingConnector} />
<Route path="/wanted/missing" component={Missing} />
<Route path="/wanted/cutoffunmet" component={CutoffUnmetConnector} />
<Route path="/wanted/cutoffunmet" component={CutoffUnmet} />
{/*
Settings
@@ -99,10 +99,7 @@ function AppRoutes() {
<Route exact={true} path="/settings" component={Settings} />
<Route
path="/settings/mediamanagement"
component={MediaManagementConnector}
/>
<Route path="/settings/mediamanagement" component={MediaManagement} />
<Route path="/settings/profiles" component={Profiles} />
@@ -113,17 +110,14 @@ function AppRoutes() {
component={CustomFormatSettingsPage}
/>
<Route path="/settings/indexers" component={IndexerSettingsConnector} />
<Route path="/settings/indexers" component={IndexerSettings} />
<Route
path="/settings/downloadclients"
component={DownloadClientSettingsConnector}
/>
<Route
path="/settings/importlists"
component={ImportListSettingsConnector}
/>
<Route path="/settings/importlists" component={ImportListSettings} />
<Route path="/settings/connect" component={NotificationSettings} />
+3 -3
View File
@@ -9,13 +9,13 @@ export type SelectContextAction =
| { type: 'unselectAll' }
| {
type: 'toggleSelected';
id: number;
isSelected: boolean;
id: number | string;
isSelected: boolean | null;
shiftKey: boolean;
}
| {
type: 'removeItem';
id: number;
id: number | string;
}
| {
type: 'updateItems';
+13 -6
View File
@@ -1,7 +1,7 @@
import Column from 'Components/Table/Column';
import { SortDirection } from 'Helpers/Props/sortDirections';
import { ValidationFailure } from 'typings/pending';
import { FilterBuilderProp, PropertyFilter } from './AppState';
import { Filter, FilterBuilderProp } from './AppState';
export interface Error {
status?: number;
@@ -35,7 +35,7 @@ export interface TableAppSectionState {
export interface AppSectionFilterState<T> {
selectedFilterKey: string;
filters: PropertyFilter[];
filters: Filter[];
filterBuilderProps: FilterBuilderProp<T>[];
}
@@ -43,9 +43,15 @@ export interface AppSectionSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: {
items: T[];
};
schema: T[];
selectedSchema?: T;
}
export interface AppSectionItemSchemaState<T> {
isSchemaFetching: boolean;
isSchemaPopulated: boolean;
schemaError: Error;
schema: T;
}
export interface AppSectionItemState<T> {
@@ -61,9 +67,10 @@ export interface AppSectionProviderState<T>
AppSectionSaveState {
isFetching: boolean;
isPopulated: boolean;
isTesting?: boolean;
error: Error;
items: T[];
pendingChanges: Partial<T>;
pendingChanges?: Partial<T>;
}
interface AppSectionState<T> {
+17 -3
View File
@@ -1,10 +1,13 @@
import { Error } from './AppSectionState';
import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState';
import CaptchaAppState from './CaptchaAppState';
import CommandAppState from './CommandAppState';
import CustomFiltersAppState from './CustomFiltersAppState';
import ExtraFilesAppState from './ExtraFilesAppState';
import HistoryAppState, { MovieHistoryAppState } from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MessagesAppState from './MessagesAppState';
import MovieBlocklistAppState from './MovieBlocklistAppState';
import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
@@ -21,6 +24,7 @@ import RootFolderAppState from './RootFolderAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
import WantedAppState from './WantedAppState';
interface FilterBuilderPropOption {
id: string;
@@ -43,28 +47,36 @@ export interface PropertyFilter {
export interface Filter {
key: string;
label: string;
filers: PropertyFilter[];
label: string | (() => string);
filters: PropertyFilter[];
}
export interface CustomFilter {
id: number;
type: string;
label: string;
filers: PropertyFilter[];
filters: PropertyFilter[];
}
export interface AppSectionState {
isUpdated: boolean;
isConnected: boolean;
isDisconnected: boolean;
isReconnecting: boolean;
isSidebarVisible: boolean;
version: string;
prevVersion?: string;
dimensions: {
isSmallScreen: boolean;
isLargeScreen: boolean;
width: number;
height: number;
};
translations: {
error?: Error;
isPopulated: boolean;
};
messages: MessagesAppState;
}
interface AppState {
@@ -73,6 +85,7 @@ interface AppState {
calendar: CalendarAppState;
captcha: CaptchaAppState;
commands: CommandAppState;
customFilters: CustomFiltersAppState;
extraFiles: ExtraFilesAppState;
history: HistoryAppState;
interactiveImport: InteractiveImportAppState;
@@ -94,6 +107,7 @@ interface AppState {
settings: SettingsAppState;
system: SystemAppState;
tags: TagsAppState;
wanted: WantedAppState;
}
export default AppState;
+22 -3
View File
@@ -1,10 +1,29 @@
import moment from 'moment';
import AppSectionState, {
AppSectionFilterState,
} 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
extends AppSectionState<Movie>,
AppSectionFilterState<Movie> {}
extends AppSectionState<CalendarItem>,
AppSectionFilterState<CalendarItem> {
searchMissingCommandId: number | null;
start: moment.Moment;
end: moment.Moment;
dates: string[];
time: string;
view: CalendarView;
options: CalendarOptions;
}
export default CalendarAppState;
@@ -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;
+44 -2
View File
@@ -1,12 +1,15 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemSchemaState,
AppSectionItemState,
AppSectionSaveState,
AppSectionSchemaState,
PagedAppSectionState,
} from 'App/State/AppSectionState';
import Language from 'Language/Language';
import AutoTagging, { AutoTaggingSpecification } from 'typings/AutoTagging';
import CustomFormat from 'typings/CustomFormat';
import DelayProfile from 'typings/DelayProfile';
import DownloadClient from 'typings/DownloadClient';
import ImportList from 'typings/ImportList';
import ImportListExclusion from 'typings/ImportListExclusion';
@@ -16,12 +19,34 @@ import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General';
import IndexerOptions from 'typings/Settings/IndexerOptions';
import MediaManagement from 'typings/Settings/MediaManagement';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings';
import MetadataAppState from './MetadataAppState';
type Presets<T> = T & {
presets: T[];
};
export interface AutoTaggingAppState
extends AppSectionState<AutoTagging>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface AutoTaggingSpecificationAppState
extends AppSectionState<AutoTaggingSpecification>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<AutoTaggingSpecification> {}
export interface DelayProfileAppState
extends AppSectionState<DelayProfile>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
@@ -33,6 +58,10 @@ export interface GeneralAppState
extends AppSectionItemState<General>,
AppSectionSaveState {}
export interface MediaManagementAppState
extends AppSectionItemState<MediaManagement>,
AppSectionSaveState {}
export interface NamingAppState
extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {}
@@ -42,12 +71,20 @@ export type NamingExamplesAppState = AppSectionItemState<NamingExample>;
export interface ImportListAppState
extends AppSectionState<ImportList>,
AppSectionDeleteState,
AppSectionSaveState,
AppSectionSchemaState<Presets<ImportList>> {
isTestingAll: boolean;
}
export interface IndexerOptionsAppState
extends AppSectionItemState<IndexerOptions>,
AppSectionSaveState {}
export interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {
AppSectionSaveState,
AppSectionSchemaState<Presets<Indexer>> {
isTestingAll: boolean;
}
@@ -57,7 +94,7 @@ export interface NotificationAppState
export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {}
AppSectionItemSchemaState<QualityProfile> {}
export interface ReleaseProfilesAppState
extends AppSectionState<ReleaseProfile>,
@@ -88,15 +125,20 @@ export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
advancedSettings: boolean;
autoTaggings: AutoTaggingAppState;
autoTaggingSpecifications: AutoTaggingSpecificationAppState;
customFormats: CustomFormatAppState;
delayProfiles: DelayProfileAppState;
downloadClients: DownloadClientAppState;
general: GeneralAppState;
importListExclusions: ImportListExclusionsSettingsAppState;
importListOptions: ImportListOptionsSettingsAppState;
importLists: ImportListAppState;
indexerFlags: IndexerFlagSettingsAppState;
indexerOptions: IndexerOptionsAppState;
indexers: IndexerAppState;
languages: LanguageSettingsAppState;
mediaManagement: MediaManagementAppState;
metadata: MetadataAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;
+4
View File
@@ -1,5 +1,6 @@
import DiskSpace from 'typings/DiskSpace';
import Health from 'typings/Health';
import LogFile from 'typings/LogFile';
import SystemStatus from 'typings/SystemStatus';
import Task from 'typings/Task';
import Update from 'typings/Update';
@@ -9,13 +10,16 @@ export type DiskSpaceAppState = AppSectionState<DiskSpace>;
export type HealthAppState = AppSectionState<Health>;
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type TaskAppState = AppSectionState<Task>;
export type LogFilesAppState = AppSectionState<LogFile>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState {
diskSpace: DiskSpaceAppState;
health: HealthAppState;
logFiles: LogFilesAppState;
status: SystemStatusAppState;
tasks: TaskAppState;
updateLogFiles: LogFilesAppState;
updates: UpdateAppState;
}
+1 -1
View File
@@ -17,7 +17,7 @@ export interface TagDetail extends ModelBase {
indexerIds: number[];
movieIds: number[];
notificationIds: number[];
restrictionIds: number[];
releaseProfileIds: number[];
}
export interface TagDetailAppState
+29
View File
@@ -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;
-69
View File
@@ -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;
+81
View File
@@ -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);
+7 -5
View File
@@ -53,6 +53,13 @@
margin-right: 10px;
}
.releaseIcon {
margin-right: 20px;
width: 25px;
cursor: default;
pointer-events: all;
}
.statusIcon {
margin-left: 3px;
cursor: default;
@@ -107,8 +114,3 @@
flex: 0 0 100%;
}
}
.releaseIcon {
margin-right: 20px;
width: 25px;
}
-190
View File
@@ -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);
-67
View File
@@ -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;
+164
View File
@@ -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;
-195
View File
@@ -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);
-224
View File
@@ -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;
+224
View File
@@ -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)
);
+48 -16
View File
@@ -1,23 +1,61 @@
import classNames from 'classnames';
import moment from 'moment';
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 CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
import CalendarEvent from 'typings/CalendarEvent';
import CalendarEvent from 'Calendar/Events/CalendarEvent';
import { CalendarEvent as CalendarEventModel } from 'typings/Calendar';
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 {
date: string;
time: string;
isTodaysDate: boolean;
events: CalendarEvent[];
view: string;
onEventModalOpenToggle(...args: unknown[]): unknown;
}
function CalendarDay(props: CalendarDayProps) {
const { date, time, isTodaysDate, events, view, onEventModalOpenToggle } =
props;
function CalendarDay({ date, isTodaysDate }: CalendarDayProps) {
const { time, view } = useSelector((state: AppState) => state.calendar);
const events = useSelector(createCalendarEventsConnector(date));
const ref = React.useRef<HTMLDivElement>(null);
@@ -50,13 +88,7 @@ function CalendarDay(props: CalendarDayProps) {
<div>
{events.map((event) => {
return (
<CalendarEventConnector
key={event.id}
{...event}
movieId={event.id}
date={date as string}
onEventModalOpenToggle={onEventModalOpenToggle}
/>
<CalendarEvent key={event.id} {...event} date={date as string} />
);
})}
</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);
-164
View File
@@ -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;
+129
View File
@@ -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);
-56
View File
@@ -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;
+54
View File
@@ -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;
-97
View File
@@ -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;
+60
View File
@@ -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,
.genres {
.genres,
.eventType {
@add-mixin truncate;
flex: 1 0 1px;
margin-right: 10px;
+1
View File
@@ -4,6 +4,7 @@ interface CssExports {
'continuing': string;
'downloaded': string;
'event': string;
'eventType': string;
'genres': string;
'info': 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);
@@ -1,56 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import QueueDetails from 'Activity/Queue/QueueDetails';
import CircularProgressBar from 'Components/CircularProgressBar';
function CalendarEventQueueDetails(props) {
const {
title,
size,
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
statusMessages,
errorMessage
} = props;
const progress = size ? (100 - sizeleft / size * 100) : 0;
return (
<QueueDetails
title={title}
size={size}
sizeleft={sizeleft}
estimatedCompletionTime={estimatedCompletionTime}
status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
statusMessages={statusMessages}
errorMessage={errorMessage}
progressBar={
<CircularProgressBar
progress={progress}
size={20}
strokeWidth={2}
strokeColor={'#7a43b6'}
/>
}
/>
);
}
CalendarEventQueueDetails.propTypes = {
title: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string
};
export default CalendarEventQueueDetails;
@@ -0,0 +1,58 @@
import React from 'react';
import QueueDetails from 'Activity/Queue/QueueDetails';
import CircularProgressBar from 'Components/CircularProgressBar';
import {
QueueTrackedDownloadState,
QueueTrackedDownloadStatus,
StatusMessage,
} from 'typings/Queue';
interface CalendarEventQueueDetailsProps {
title: string;
size: number;
sizeleft: number;
estimatedCompletionTime?: string;
status: string;
trackedDownloadState: QueueTrackedDownloadState;
trackedDownloadStatus: QueueTrackedDownloadStatus;
statusMessages?: StatusMessage[];
errorMessage?: string;
}
function CalendarEventQueueDetails({
title,
size,
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
statusMessages,
errorMessage,
}: CalendarEventQueueDetailsProps) {
const progress = size ? 100 - (sizeleft / size) * 100 : 0;
return (
<QueueDetails
title={title}
size={size}
sizeleft={sizeleft}
estimatedCompletionTime={estimatedCompletionTime}
status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
statusMessages={statusMessages}
errorMessage={errorMessage}
progressBar={
<CircularProgressBar
progress={progress}
size={20}
strokeWidth={2}
strokeColor="#7a43b6"
/>
}
/>
);
}
export default CalendarEventQueueDetails;
@@ -1,268 +0,0 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align, icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
import styles from './CalendarHeader.css';
function getTitle(time, start, end, view, longDateFormat) {
const timeMoment = moment(time);
const startMoment = moment(start);
const endMoment = moment(end);
if (view === 'day') {
return timeMoment.format(longDateFormat);
} else if (view === 'month') {
return timeMoment.format('MMMM YYYY');
} else if (view === 'agenda') {
return `Agenda: ${startMoment.format('MMM D')} - ${endMoment.format('MMM D')}`;
}
let startFormat = 'MMM D YYYY';
let endFormat = 'MMM D YYYY';
if (startMoment.isSame(endMoment, 'month')) {
startFormat = 'MMM D';
endFormat = 'D YYYY';
} else if (startMoment.isSame(endMoment, 'year')) {
startFormat = 'MMM D';
endFormat = 'MMM D YYYY';
}
return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(endFormat)}`;
}
// TODO Convert to a stateful Component so we can track view internally when changed
class CalendarHeader extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
view: props.view
};
}
componentDidUpdate(prevProps) {
const view = this.props.view;
if (prevProps.view !== view) {
this.setState({ view });
}
}
//
// Listeners
onViewChange = (view) => {
this.setState({ view }, () => {
this.props.onViewChange(view);
});
};
//
// Render
render() {
const {
isFetching,
time,
start,
end,
longDateFormat,
isSmallScreen,
collapseViewButtons,
onTodayPress,
onPreviousPress,
onNextPress
} = this.props;
const view = this.state.view;
const title = getTitle(time, start, end, view, longDateFormat);
return (
<div>
{
isSmallScreen &&
<div className={styles.titleMobile}>
{title}
</div>
}
<div className={styles.header}>
<div className={styles.navigationButtons}>
<Button
buttonGroupPosition={align.LEFT}
isDisabled={view === calendarViews.AGENDA}
onPress={onPreviousPress}
>
<Icon name={icons.PAGE_PREVIOUS} />
</Button>
<Button
buttonGroupPosition={align.RIGHT}
isDisabled={view === calendarViews.AGENDA}
onPress={onNextPress}
>
<Icon name={icons.PAGE_NEXT} />
</Button>
<Button
className={styles.todayButton}
isDisabled={view === calendarViews.AGENDA}
onPress={onTodayPress}
>
{translate('Today')}
</Button>
</div>
{
!isSmallScreen &&
<div className={styles.titleDesktop}>
{title}
</div>
}
<div className={styles.viewButtonsContainer}>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
collapseViewButtons ?
<Menu
className={styles.viewMenu}
alignMenu={align.RIGHT}
>
<MenuButton>
<Icon
name={icons.VIEW}
size={22}
/>
</MenuButton>
<MenuContent>
{
isSmallScreen ?
null :
<ViewMenuItem
name={calendarViews.MONTH}
selectedView={view}
onPress={this.onViewChange}
>
{translate('Month')}
</ViewMenuItem>
}
<ViewMenuItem
name={calendarViews.WEEK}
selectedView={view}
onPress={this.onViewChange}
>
{translate('Week')}
</ViewMenuItem>
<ViewMenuItem
name={calendarViews.FORECAST}
selectedView={view}
onPress={this.onViewChange}
>
{translate('Forecast')}
</ViewMenuItem>
<ViewMenuItem
name={calendarViews.DAY}
selectedView={view}
onPress={this.onViewChange}
>
{translate('Day')}
</ViewMenuItem>
<ViewMenuItem
name={calendarViews.AGENDA}
selectedView={view}
onPress={this.onViewChange}
>
{translate('Agenda')}
</ViewMenuItem>
</MenuContent>
</Menu> :
<div className={styles.viewButtons}>
<CalendarHeaderViewButton
view={calendarViews.MONTH}
selectedView={view}
buttonGroupPosition={align.LEFT}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.WEEK}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.FORECAST}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.DAY}
selectedView={view}
buttonGroupPosition={align.CENTER}
onPress={this.onViewChange}
/>
<CalendarHeaderViewButton
view={calendarViews.AGENDA}
selectedView={view}
buttonGroupPosition={align.RIGHT}
onPress={this.onViewChange}
/>
</div>
}
</div>
</div>
</div>
);
}
}
CalendarHeader.propTypes = {
isFetching: PropTypes.bool.isRequired,
time: PropTypes.string.isRequired,
start: PropTypes.string.isRequired,
end: PropTypes.string.isRequired,
view: PropTypes.oneOf(calendarViews.all).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
collapseViewButtons: PropTypes.bool.isRequired,
longDateFormat: PropTypes.string.isRequired,
onViewChange: PropTypes.func.isRequired,
onTodayPress: PropTypes.func.isRequired,
onPreviousPress: PropTypes.func.isRequired,
onNextPress: PropTypes.func.isRequired
};
export default CalendarHeader;
@@ -0,0 +1,218 @@
import moment from 'moment';
import React, { useCallback, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Menu from 'Components/Menu/Menu';
import MenuButton from 'Components/Menu/MenuButton';
import MenuContent from 'Components/Menu/MenuContent';
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
import { align, icons } from 'Helpers/Props';
import {
gotoCalendarNextRange,
gotoCalendarPreviousRange,
gotoCalendarToday,
setCalendarView,
} from 'Store/Actions/calendarActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import translate from 'Utilities/String/translate';
import CalendarHeaderViewButton from './CalendarHeaderViewButton';
import styles from './CalendarHeader.css';
function CalendarHeader() {
const dispatch = useDispatch();
const { isFetching, view, time, start, end } = useSelector(
(state: AppState) => state.calendar
);
const { isSmallScreen, isLargeScreen } = useSelector(
createDimensionsSelector()
);
const { longDateFormat } = useSelector(createUISettingsSelector());
const handleViewChange = useCallback(
(newView: string) => {
dispatch(setCalendarView({ view: newView }));
},
[dispatch]
);
const handleTodayPress = useCallback(() => {
dispatch(gotoCalendarToday());
}, [dispatch]);
const handlePreviousPress = useCallback(() => {
dispatch(gotoCalendarPreviousRange());
}, [dispatch]);
const handleNextPress = useCallback(() => {
dispatch(gotoCalendarNextRange());
}, [dispatch]);
const title = useMemo(() => {
const timeMoment = moment(time);
const startMoment = moment(start);
const endMoment = moment(end);
if (view === 'day') {
return timeMoment.format(longDateFormat);
} else if (view === 'month') {
return timeMoment.format('MMMM YYYY');
}
let startFormat = 'MMM D YYYY';
let endFormat = 'MMM D YYYY';
if (startMoment.isSame(endMoment, 'month')) {
startFormat = 'MMM D';
endFormat = 'D YYYY';
} else if (startMoment.isSame(endMoment, 'year')) {
startFormat = 'MMM D';
endFormat = 'MMM D YYYY';
}
return `${startMoment.format(startFormat)} \u2014 ${endMoment.format(
endFormat
)}`;
}, [time, start, end, view, longDateFormat]);
return (
<div>
{isSmallScreen ? <div className={styles.titleMobile}>{title}</div> : null}
<div className={styles.header}>
<div className={styles.navigationButtons}>
<Button
buttonGroupPosition="left"
isDisabled={view === 'agenda'}
onPress={handlePreviousPress}
>
<Icon name={icons.PAGE_PREVIOUS} />
</Button>
<Button
buttonGroupPosition="right"
isDisabled={view === 'agenda'}
onPress={handleNextPress}
>
<Icon name={icons.PAGE_NEXT} />
</Button>
<Button
className={styles.todayButton}
isDisabled={view === 'agenda'}
onPress={handleTodayPress}
>
{translate('Today')}
</Button>
</div>
{isSmallScreen ? null : (
<div className={styles.titleDesktop}>{title}</div>
)}
<div className={styles.viewButtonsContainer}>
{isFetching ? (
<LoadingIndicator className={styles.loading} size={20} />
) : null}
{isLargeScreen ? (
<Menu className={styles.viewMenu} alignMenu={align.RIGHT}>
<MenuButton>
<Icon name={icons.VIEW} size={22} />
</MenuButton>
<MenuContent>
{isSmallScreen ? null : (
<ViewMenuItem
name="month"
selectedView={view}
onPress={handleViewChange}
>
{translate('Month')}
</ViewMenuItem>
)}
<ViewMenuItem
name="week"
selectedView={view}
onPress={handleViewChange}
>
{translate('Week')}
</ViewMenuItem>
<ViewMenuItem
name="forecast"
selectedView={view}
onPress={handleViewChange}
>
{translate('Forecast')}
</ViewMenuItem>
<ViewMenuItem
name="day"
selectedView={view}
onPress={handleViewChange}
>
{translate('Day')}
</ViewMenuItem>
<ViewMenuItem
name="agenda"
selectedView={view}
onPress={handleViewChange}
>
{translate('Agenda')}
</ViewMenuItem>
</MenuContent>
</Menu>
) : (
<>
<CalendarHeaderViewButton
view="month"
selectedView={view}
buttonGroupPosition="left"
onPress={handleViewChange}
/>
<CalendarHeaderViewButton
view="week"
selectedView={view}
buttonGroupPosition="center"
onPress={handleViewChange}
/>
<CalendarHeaderViewButton
view="forecast"
selectedView={view}
buttonGroupPosition="center"
onPress={handleViewChange}
/>
<CalendarHeaderViewButton
view="day"
selectedView={view}
buttonGroupPosition="center"
onPress={handleViewChange}
/>
<CalendarHeaderViewButton
view="agenda"
selectedView={view}
buttonGroupPosition="right"
onPress={handleViewChange}
/>
</>
)}
</div>
</div>
</div>
);
}
export default CalendarHeader;
@@ -1,81 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { gotoCalendarNextRange, gotoCalendarPreviousRange, gotoCalendarToday, setCalendarView } from 'Store/Actions/calendarActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CalendarHeader from './CalendarHeader';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar,
createDimensionsSelector(),
createUISettingsSelector(),
(calendar, dimensions, uiSettings) => {
return {
isFetching: calendar.isFetching,
view: calendar.view,
time: calendar.time,
start: calendar.start,
end: calendar.end,
isSmallScreen: dimensions.isSmallScreen,
collapseViewButtons: dimensions.isLargeScreen,
longDateFormat: uiSettings.longDateFormat
};
}
);
}
const mapDispatchToProps = {
setCalendarView,
gotoCalendarToday,
gotoCalendarPreviousRange,
gotoCalendarNextRange
};
class CalendarHeaderConnector extends Component {
//
// Listeners
onViewChange = (view) => {
this.props.setCalendarView({ view });
};
onTodayPress = () => {
this.props.gotoCalendarToday();
};
onPreviousPress = () => {
this.props.gotoCalendarPreviousRange();
};
onNextPress = () => {
this.props.gotoCalendarNextRange();
};
//
// Render
render() {
return (
<CalendarHeader
{...this.props}
onViewChange={this.onViewChange}
onTodayPress={this.onTodayPress}
onPreviousPress={this.onPreviousPress}
onNextPress={this.onNextPress}
/>
);
}
}
CalendarHeaderConnector.propTypes = {
setCalendarView: PropTypes.func.isRequired,
gotoCalendarToday: PropTypes.func.isRequired,
gotoCalendarPreviousRange: PropTypes.func.isRequired,
gotoCalendarNextRange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarHeaderConnector);
@@ -1,45 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import * as calendarViews from 'Calendar/calendarViews';
import Button from 'Components/Link/Button';
import titleCase from 'Utilities/String/titleCase';
// import styles from './CalendarHeaderViewButton.css';
class CalendarHeaderViewButton extends Component {
//
// Listeners
onPress = () => {
this.props.onPress(this.props.view);
};
//
// Render
render() {
const {
view,
selectedView,
...otherProps
} = this.props;
return (
<Button
isDisabled={selectedView === view}
{...otherProps}
onPress={this.onPress}
>
{titleCase(view)}
</Button>
);
}
}
CalendarHeaderViewButton.propTypes = {
view: PropTypes.oneOf(calendarViews.all).isRequired,
selectedView: PropTypes.oneOf(calendarViews.all).isRequired,
onPress: PropTypes.func.isRequired
};
export default CalendarHeaderViewButton;
@@ -0,0 +1,34 @@
import React, { useCallback } from 'react';
import { CalendarView } from 'Calendar/calendarViews';
import Button, { ButtonProps } from 'Components/Link/Button';
import titleCase from 'Utilities/String/titleCase';
interface CalendarHeaderViewButtonProps
extends Omit<ButtonProps, 'children' | 'onPress'> {
view: CalendarView;
selectedView: CalendarView;
onPress: (view: CalendarView) => void;
}
function CalendarHeaderViewButton({
view,
selectedView,
onPress,
...otherProps
}: CalendarHeaderViewButtonProps) {
const handlePress = useCallback(() => {
onPress(view);
}, [view, onPress]);
return (
<Button
isDisabled={selectedView === view}
{...otherProps}
onPress={handlePress}
>
{titleCase(view)}
</Button>
);
}
export default CalendarHeaderViewButton;
@@ -1,18 +1,19 @@
import PropTypes from 'prop-types';
import React from 'react';
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { icons, kinds } from 'Helpers/Props';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import translate from 'Utilities/String/translate';
import LegendIconItem from './LegendIconItem';
import LegendItem from './LegendItem';
import styles from './Legend.css';
function Legend(props) {
const {
view,
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode
} = props;
function Legend() {
const view = useSelector((state: AppState) => state.calendar.view);
const { showCutoffUnmetIcon, fullColorEvents } = useSelector(
(state: AppState) => state.calendar.options
);
const { enableColorImpairedMode } = useSelector(createUISettingsSelector());
const iconsToShow = [];
const isAgendaView = view === 'agenda';
@@ -37,7 +38,7 @@ function Legend(props) {
name={translate('DownloadedAndMonitored')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
colorImpairedMode={enableColorImpairedMode}
/>
<LegendItem
@@ -45,7 +46,7 @@ function Legend(props) {
name={translate('DownloadedButNotMonitored')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
colorImpairedMode={enableColorImpairedMode}
/>
</div>
@@ -55,7 +56,7 @@ function Legend(props) {
name={translate('MissingMonitoredAndConsideredAvailable')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
colorImpairedMode={enableColorImpairedMode}
/>
<LegendItem
@@ -63,7 +64,7 @@ function Legend(props) {
name={translate('MissingNotMonitored')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
colorImpairedMode={enableColorImpairedMode}
/>
</div>
@@ -73,7 +74,7 @@ function Legend(props) {
name={translate('Queued')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
colorImpairedMode={enableColorImpairedMode}
/>
<LegendItem
@@ -81,25 +82,13 @@ function Legend(props) {
name={translate('Unreleased')}
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
colorImpairedMode={enableColorImpairedMode}
/>
</div>
{
iconsToShow.length > 0 &&
<div>
{iconsToShow[0]}
</div>
}
{iconsToShow.length > 0 ? <div>{iconsToShow[0]}</div> : null}
</div>
);
}
Legend.propTypes = {
view: PropTypes.string.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
export default Legend;
@@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.calendar.view,
createUISettingsSelector(),
(calendarOptions, view, uiSettings) => {
return {
...calendarOptions,
view,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
}
export default connect(createMapStateToProps)(Legend);
@@ -1,43 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './LegendIconItem.css';
function LegendIconItem(props) {
const {
name,
fullColorEvents,
icon,
kind,
tooltip
} = props;
return (
<div
className={styles.legendIconItem}
title={tooltip}
>
<Icon
className={classNames(
styles.icon,
fullColorEvents && 'fullColorEvents'
)}
name={icon}
kind={kind}
/>
{name}
</div>
);
}
LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired
};
export default LegendIconItem;
@@ -0,0 +1,33 @@
import { FontAwesomeIconProps } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
import React from 'react';
import Icon, { IconProps } from 'Components/Icon';
import styles from './LegendIconItem.css';
interface LegendIconItemProps extends Pick<IconProps, 'kind'> {
name: string;
fullColorEvents: boolean;
icon: FontAwesomeIconProps['icon'];
tooltip: string;
}
function LegendIconItem(props: LegendIconItemProps) {
const { name, fullColorEvents, icon, kind, tooltip } = props;
return (
<div className={styles.legendIconItem} title={tooltip}>
<Icon
className={classNames(
styles.icon,
fullColorEvents && 'fullColorEvents'
)}
name={icon}
kind={kind}
/>
{name}
</div>
);
}
export default LegendIconItem;
@@ -1,37 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import styles from './LegendItem.css';
function LegendItem(props) {
const {
name,
status,
isAgendaView,
fullColorEvents,
colorImpairedMode
} = props;
return (
<div
className={classNames(
styles.legendItem,
styles[status],
colorImpairedMode && 'colorImpaired',
fullColorEvents && !isAgendaView && 'fullColor'
)}
>
{name}
</div>
);
}
LegendItem.propTypes = {
name: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
isAgendaView: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
export default LegendItem;
@@ -0,0 +1,35 @@
import classNames from 'classnames';
import React from 'react';
import { CalendarStatus } from 'typings/Calendar';
import styles from './LegendItem.css';
interface LegendItemProps {
name: string;
status: CalendarStatus;
isAgendaView: boolean;
fullColorEvents: boolean;
colorImpairedMode: boolean;
}
function LegendItem({
name,
status,
isAgendaView,
fullColorEvents,
colorImpairedMode,
}: LegendItemProps) {
return (
<div
className={classNames(
styles.legendItem,
styles[status],
colorImpairedMode && 'colorImpaired',
fullColorEvents && !isAgendaView && 'fullColor'
)}
>
{name}
</div>
);
}
export default LegendItem;
@@ -1,29 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CalendarOptionsModalContentConnector from './CalendarOptionsModalContentConnector';
function CalendarOptionsModal(props) {
const {
isOpen,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<CalendarOptionsModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
CalendarOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarOptionsModal;
@@ -0,0 +1,21 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CalendarOptionsModalContent from './CalendarOptionsModalContent';
interface CalendarOptionsModalProps {
isOpen: boolean;
onModalClose: () => void;
}
function CalendarOptionsModal({
isOpen,
onModalClose,
}: CalendarOptionsModalProps) {
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<CalendarOptionsModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default CalendarOptionsModal;
@@ -1,234 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import { firstDayOfWeekOptions, timeFormatOptions, weekColumnOptions } from 'Settings/UI/UISettings';
import translate from 'Utilities/String/translate';
class CalendarOptionsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode,
fullColorEvents
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode,
fullColorEvents
};
}
componentDidUpdate(prevProps) {
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = this.props;
if (
prevProps.firstDayOfWeek !== firstDayOfWeek ||
prevProps.calendarWeekColumnHeader !== calendarWeekColumnHeader ||
prevProps.timeFormat !== timeFormat ||
prevProps.enableColorImpairedMode !== enableColorImpairedMode
) {
this.setState({
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
});
}
}
//
// Listeners
onOptionInputChange = ({ name, value }) => {
const {
dispatchSetCalendarOption
} = this.props;
dispatchSetCalendarOption({ [name]: value });
};
onGlobalInputChange = ({ name, value }) => {
const {
dispatchSaveUISettings
} = this.props;
const setting = { [name]: value };
this.setState(setting, () => {
dispatchSaveUISettings(setting);
});
};
onLinkFocus = (event) => {
event.target.select();
};
//
// Render
render() {
const {
showMovieInformation,
showCutoffUnmetIcon,
fullColorEvents,
onModalClose
} = this.props;
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('CalendarOptions')}
</ModalHeader>
<ModalBody>
<FieldSet legend={translate('Local')}>
<Form>
<FormGroup>
<FormLabel>{translate('ShowMovieInformation')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showMovieInformation"
value={showMovieInformation}
helpText={translate('ShowMovieInformationHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('IconForCutoffUnmet')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showCutoffUnmetIcon"
value={showCutoffUnmetIcon}
helpText={translate('IconForCutoffUnmetHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('FullColorEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText={translate('FullColorEventsHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
<FieldSet legend={translate('Global')}>
<Form>
<FormGroup>
<FormLabel>{translate('FirstDayOfWeek')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="firstDayOfWeek"
values={firstDayOfWeekOptions}
value={firstDayOfWeek}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('WeekColumnHeader')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="calendarWeekColumnHeader"
values={weekColumnOptions}
value={calendarWeekColumnHeader}
onChange={this.onGlobalInputChange}
helpText={translate('WeekColumnHeaderHelpText')}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('TimeFormat')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="timeFormat"
values={timeFormatOptions}
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableColorImpairedMode"
value={enableColorImpairedMode}
helpText={translate('EnableColorImpairedModeHelpText')}
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
CalendarOptionsModalContent.propTypes = {
showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
firstDayOfWeek: PropTypes.number.isRequired,
calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarOptionsModalContent;
@@ -0,0 +1,243 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import FieldSet from 'Components/FieldSet';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props';
import {
firstDayOfWeekOptions,
timeFormatOptions,
weekColumnOptions,
} from 'Settings/UI/UISettings';
import { setCalendarOption } from 'Store/Actions/calendarActions';
import { saveUISettings } from 'Store/Actions/settingsActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import { InputChanged } from 'typings/inputs';
import UiSettings from 'typings/Settings/UiSettings';
import translate from 'Utilities/String/translate';
interface CalendarOptionsModalContentProps {
onModalClose: () => void;
}
function CalendarOptionsModalContent({
onModalClose,
}: CalendarOptionsModalContentProps) {
const dispatch = useDispatch();
const {
showMovieInformation,
showCinemaRelease,
showDigitalRelease,
showPhysicalRelease,
showCutoffUnmetIcon,
fullColorEvents,
} = useSelector((state: AppState) => state.calendar.options);
const uiSettings = useSelector(createUISettingsSelector());
const [state, setState] = useState<Partial<UiSettings>>({
firstDayOfWeek: uiSettings.firstDayOfWeek,
calendarWeekColumnHeader: uiSettings.calendarWeekColumnHeader,
timeFormat: uiSettings.timeFormat,
enableColorImpairedMode: uiSettings.enableColorImpairedMode,
});
const {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode,
} = state;
const handleOptionInputChange = useCallback(
({ name, value }: InputChanged) => {
dispatch(setCalendarOption({ [name]: value }));
},
[dispatch]
);
const handleGlobalInputChange = useCallback(
({ name, value }: InputChanged) => {
setState((prevState) => ({ ...prevState, [name]: value }));
dispatch(saveUISettings({ [name]: value }));
},
[dispatch]
);
useEffect(() => {
setState({
firstDayOfWeek: uiSettings.firstDayOfWeek,
calendarWeekColumnHeader: uiSettings.calendarWeekColumnHeader,
timeFormat: uiSettings.timeFormat,
enableColorImpairedMode: uiSettings.enableColorImpairedMode,
});
}, [uiSettings]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('CalendarOptions')}</ModalHeader>
<ModalBody>
<FieldSet legend={translate('Local')}>
<Form>
<FormGroup>
<FormLabel>{translate('ShowMovieInformation')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showMovieInformation"
value={showMovieInformation}
helpText={translate('ShowMovieInformationHelpText')}
onChange={handleOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowCinemaRelease')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showCinemaRelease"
value={showCinemaRelease}
helpText={translate('ShowCinemaReleaseCalendarHelpText')}
isDisabled={
showCinemaRelease &&
!showDigitalRelease &&
!showPhysicalRelease
}
onChange={handleOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowDigitalRelease')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showDigitalRelease"
value={showDigitalRelease}
helpText={translate('ShowDigitalReleaseCalendarHelpText')}
isDisabled={
!showCinemaRelease &&
showDigitalRelease &&
!showPhysicalRelease
}
onChange={handleOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowPhysicalRelease')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showPhysicalRelease"
value={showPhysicalRelease}
helpText={translate('ShowPhysicalReleaseCalendarHelpText')}
isDisabled={
!showCinemaRelease &&
!showDigitalRelease &&
showPhysicalRelease
}
onChange={handleOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('IconForCutoffUnmet')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showCutoffUnmetIcon"
value={showCutoffUnmetIcon}
helpText={translate('IconForCutoffUnmetHelpText')}
onChange={handleOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('FullColorEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText={translate('FullColorEventsHelpText')}
onChange={handleOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
<FieldSet legend={translate('Global')}>
<Form>
<FormGroup>
<FormLabel>{translate('FirstDayOfWeek')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="firstDayOfWeek"
values={firstDayOfWeekOptions}
value={firstDayOfWeek}
onChange={handleGlobalInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('WeekColumnHeader')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="calendarWeekColumnHeader"
values={weekColumnOptions}
value={calendarWeekColumnHeader}
helpText={translate('WeekColumnHeaderHelpText')}
onChange={handleGlobalInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('TimeFormat')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="timeFormat"
values={timeFormatOptions}
value={timeFormat}
onChange={handleGlobalInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="enableColorImpairedMode"
value={enableColorImpairedMode}
helpText={translate('EnableColorImpairedModeHelpText')}
onChange={handleGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default CalendarOptionsModalContent;
@@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setCalendarOption } from 'Store/Actions/calendarActions';
import { saveUISettings } from 'Store/Actions/settingsActions';
import CalendarOptionsModalContent from './CalendarOptionsModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.settings.ui.item,
(options, uiSettings) => {
return {
...options,
...uiSettings
};
}
);
}
const mapDispatchToProps = {
dispatchSetCalendarOption: setCalendarOption,
dispatchSaveUISettings: saveUISettings
};
export default connect(createMapStateToProps, mapDispatchToProps)(CalendarOptionsModalContent);
@@ -5,3 +5,5 @@ export const FORECAST = 'forecast';
export const AGENDA = 'agenda';
export const all = [DAY, WEEK, MONTH, FORECAST, AGENDA];
export type CalendarView = 'agenda' | 'day' | 'forecast' | 'month' | 'week';
@@ -1,4 +1,9 @@
function getStatusStyle(hasFile, downloading, isMonitored, isAvailable) {
function getStatusStyle(
hasFile: boolean,
downloading: boolean,
isMonitored: boolean,
isAvailable: boolean
) {
if (downloading) {
return 'queue';
}
@@ -1,29 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CalendarLinkModalContentConnector from './CalendarLinkModalContentConnector';
function CalendarLinkModal(props) {
const {
isOpen,
onModalClose
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<CalendarLinkModalContentConnector
onModalClose={onModalClose}
/>
</Modal>
);
}
CalendarLinkModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarLinkModal;
@@ -0,0 +1,20 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CalendarLinkModalContent from './CalendarLinkModalContent';
interface CalendarLinkModalProps {
isOpen: boolean;
onModalClose: () => void;
}
function CalendarLinkModal(props: CalendarLinkModalProps) {
const { isOpen, onModalClose } = props;
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<CalendarLinkModalContent onModalClose={onModalClose} />
</Modal>
);
}
export default CalendarLinkModal;
@@ -1,203 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import ClipboardButton from 'Components/Link/ClipboardButton';
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 { icons, inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function getUrls(state) {
const {
unmonitored,
asAllDay,
tags
} = state;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
if (unmonitored) {
icalUrl += 'unmonitored=true&';
}
if (asAllDay) {
icalUrl += 'asAllDay=true&';
}
if (tags.length) {
icalUrl += `tags=${tags.toString()}&`;
}
icalUrl += `apikey=${encodeURIComponent(window.Radarr.apiKey)}`;
const iCalHttpUrl = `${window.location.protocol}//${icalUrl}`;
const iCalWebCalUrl = `webcal://${icalUrl}`;
return {
iCalHttpUrl,
iCalWebCalUrl
};
}
class CalendarLinkModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
const defaultState = {
unmonitored: false,
asAllDay: false,
tags: []
};
const urls = getUrls(defaultState);
this.state = {
...defaultState,
...urls
};
}
//
// Listeners
onInputChange = ({ name, value }) => {
const state = {
...this.state,
[name]: value
};
const urls = getUrls(state);
this.setState({
[name]: value,
...urls
});
};
onLinkFocus = (event) => {
event.target.select();
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
unmonitored,
asAllDay,
tags,
iCalHttpUrl,
iCalWebCalUrl
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('CalendarFeed')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('IncludeUnmonitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="unmonitored"
value={unmonitored}
helpText={translate('ICalIncludeUnmonitoredMoviesHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="asAllDay"
value={asAllDay}
helpText={translate('ICalShowAsAllDayEventsHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
value={tags}
helpText={translate('ICalTagsMoviesHelpText')}
onChange={this.onInputChange}
/>
</FormGroup>
<FormGroup
size={sizes.LARGE}
>
<FormLabel>{translate('ICalFeed')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="iCalHttpUrl"
value={iCalHttpUrl}
readOnly={true}
helpText={translate('ICalFeedHelpText')}
buttons={[
<ClipboardButton
key="copy"
value={iCalHttpUrl}
kind={kinds.DEFAULT}
/>,
<FormInputButton
key="webcal"
kind={kinds.DEFAULT}
to={iCalWebCalUrl}
target="_blank"
noRouter={true}
>
<Icon name={icons.CALENDAR_O} />
</FormInputButton>
]}
onChange={this.onInputChange}
onFocus={this.onLinkFocus}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
CalendarLinkModalContent.propTypes = {
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CalendarLinkModalContent;
@@ -0,0 +1,196 @@
import React, { FocusEvent, useCallback, useMemo, useState } from 'react';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
import FormInputButton from 'Components/Form/FormInputButton';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { EnhancedSelectInputValue } from 'Components/Form/Select/EnhancedSelectInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import ClipboardButton from 'Components/Link/ClipboardButton';
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 { icons, inputTypes, kinds, sizes } from 'Helpers/Props';
import { InputChanged } from 'typings/inputs';
import translate from 'Utilities/String/translate';
const releaseTypeOptions: EnhancedSelectInputValue<string>[] = [
{
key: 'cinemaRelease',
get value() {
return translate('CinemaRelease');
},
},
{
key: 'digitalRelease',
get value() {
return translate('DigitalRelease');
},
},
{
key: 'physicalRelease',
get value() {
return translate('PhysicalRelease');
},
},
];
interface CalendarLinkModalContentProps {
onModalClose: () => void;
}
function CalendarLinkModalContent({
onModalClose,
}: CalendarLinkModalContentProps) {
const [state, setState] = useState<{
unmonitored: boolean;
asAllDay: boolean;
releaseTypes: string[];
tags: number[];
}>({
unmonitored: false,
asAllDay: false,
releaseTypes: [],
tags: [],
});
const { unmonitored, asAllDay, releaseTypes, tags } = state;
const handleInputChange = useCallback(({ name, value }: InputChanged) => {
setState((prevState) => ({ ...prevState, [name]: value }));
}, []);
const handleLinkFocus = useCallback(
(event: FocusEvent<HTMLInputElement, Element>) => {
event.target.select();
},
[]
);
const { iCalHttpUrl, iCalWebCalUrl } = useMemo(() => {
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
if (unmonitored) {
icalUrl += 'unmonitored=true&';
}
if (asAllDay) {
icalUrl += 'asAllDay=true&';
}
if (releaseTypes.length) {
releaseTypes.forEach((releaseType) => {
icalUrl += `releaseTypes=${releaseType}&`;
});
}
if (tags.length) {
icalUrl += `tags=${tags.toString()}&`;
}
icalUrl += `apikey=${encodeURIComponent(window.Radarr.apiKey)}`;
return {
iCalHttpUrl: `${window.location.protocol}//${icalUrl}`,
iCalWebCalUrl: `webcal://${icalUrl}`,
};
}, [unmonitored, asAllDay, releaseTypes, tags]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{translate('CalendarFeed')}</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('IncludeUnmonitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="unmonitored"
value={unmonitored}
helpText={translate('ICalIncludeUnmonitoredMoviesHelpText')}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="asAllDay"
value={asAllDay}
helpText={translate('ICalShowAsAllDayEventsHelpText')}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ICalReleaseTypes')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="releaseTypes"
value={releaseTypes}
values={releaseTypeOptions}
helpText={translate('ICalReleaseTypesMoviesHelpText')}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.MOVIE_TAG}
name="tags"
value={tags}
helpText={translate('ICalTagsMoviesHelpText')}
onChange={handleInputChange}
/>
</FormGroup>
<FormGroup size={sizes.LARGE}>
<FormLabel>{translate('ICalFeed')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="iCalHttpUrl"
value={iCalHttpUrl}
readOnly={true}
helpText={translate('ICalFeedHelpText')}
buttons={[
<ClipboardButton
key="copy"
value={iCalHttpUrl}
kind={kinds.DEFAULT}
/>,
<FormInputButton
key="webcal"
kind={kinds.DEFAULT}
to={iCalWebCalUrl}
target="_blank"
noRouter={true}
>
<Icon name={icons.CALENDAR_O} />
</FormInputButton>,
]}
onChange={handleInputChange}
onFocus={handleLinkFocus}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
);
}
export default CalendarLinkModalContent;
@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import CalendarLinkModalContent from './CalendarLinkModalContent';
function createMapStateToProps() {
return createSelector(
createTagsSelector(),
(tagList) => {
return {
tagList
};
}
);
}
export default connect(createMapStateToProps)(CalendarLinkModalContent);
@@ -1,6 +1,7 @@
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'Components/Modal/Modal';
import usePrevious from 'Helpers/Hooks/usePrevious';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import AddNewMovieCollectionMovieModalContent, {
AddNewMovieCollectionMovieModalContentProps,
@@ -18,11 +19,19 @@ function AddNewMovieCollectionMovieModal({
}: AddNewCollectionMovieModalProps) {
const dispatch = useDispatch();
const wasOpen = usePrevious(isOpen);
const handleModalClose = useCallback(() => {
dispatch(clearPendingChanges({ section: 'movieCollections' }));
onModalClose();
}, [dispatch, onModalClose]);
useEffect(() => {
if (wasOpen && !isOpen) {
dispatch(clearPendingChanges({ section: 'movieCollections' }));
}
}, [wasOpen, isOpen, dispatch]);
return (
<Modal isOpen={isOpen} onModalClose={handleModalClose}>
<AddNewMovieCollectionMovieModalContent
+5 -1
View File
@@ -224,6 +224,7 @@ class Collection extends Component {
view,
onSortSelect,
onFilterSelect,
initialScrollTop,
onScroll,
isRefreshingCollections,
isSaving,
@@ -247,7 +248,7 @@ class Collection extends Component {
const hasNoCollection = !totalItems;
return (
<PageContent>
<PageContent title={translate('Collections')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
@@ -306,6 +307,7 @@ class Collection extends Component {
ref={this.scrollerRef}
className={styles.contentBody}
innerClassName={styles[`${view}InnerContentBody`]}
onScroll={onScroll}
>
{
isFetching && !isPopulated &&
@@ -334,6 +336,7 @@ class Collection extends Component {
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
scrollTop={initialScrollTop}
{...otherProps}
/>
</div>
@@ -374,6 +377,7 @@ class Collection extends Component {
}
Collection.propTypes = {
initialScrollTop: PropTypes.number,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
+18 -10
View File
@@ -5,14 +5,17 @@ import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition';
import { executeCommand } from 'Store/Actions/commandActions';
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
import {
fetchMovieCollections,
saveMovieCollections,
setMovieCollectionsFilter,
setMovieCollectionsSort
} from 'Store/Actions/movieCollectionActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import scrollPositions from 'Store/scrollPositions';
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Collection from './Collection';
function createMapStateToProps() {
@@ -36,8 +39,8 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchRootFolders() {
dispatch(fetchRootFolders());
dispatchFetchMovieCollections() {
dispatch(fetchMovieCollections());
},
dispatchFetchQueueDetails() {
dispatch(fetchQueueDetails());
@@ -68,13 +71,11 @@ class CollectionConnector extends Component {
// Lifecycle
componentDidMount() {
registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders();
this.props.dispatchFetchMovieCollections();
this.props.dispatchFetchQueueDetails();
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.dispatchClearQueueDetails();
}
@@ -93,9 +94,16 @@ class CollectionConnector extends Component {
// Render
render() {
const {
dispatchFetchMovieCollections,
dispatchFetchQueueDetails,
dispatchClearQueueDetails,
...otherProps
} = this.props;
return (
<Collection
{...this.props}
{...otherProps}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onUpdateSelectedPress={this.onUpdateSelectedPress}
@@ -108,7 +116,7 @@ CollectionConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
onUpdateSelectedPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchFetchMovieCollections: PropTypes.func.isRequired,
dispatchFetchQueueDetails: PropTypes.func.isRequired,
dispatchClearQueueDetails: PropTypes.func.isRequired
};
@@ -9,12 +9,13 @@ $hoverScale: 1.05;
box-shadow: 0 0 10px var(--black);
transition: all 200ms ease-in;
.poster {
.poster,
.overlayTitle {
opacity: 0.5;
transition: opacity 100ms linear 100ms;
}
.overlayTitle {
.overlayHoverTitle {
opacity: 1;
transition: opacity 100ms linear 100ms;
}
@@ -31,7 +32,22 @@ $hoverScale: 1.05;
background-color: var(--defaultColor);
}
.overlay {
.overlayTitle {
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
width: 100%;
height: 100%;
color: var(--offWhite);
text-align: center;
font-size: 20px;
}
.overlayHover {
position: absolute;
top: 0;
left: 0;
@@ -42,10 +58,10 @@ $hoverScale: 1.05;
height: 100%;
}
.overlayTitle {
.overlayHoverTitle {
padding: 5px;
color: var(--offWhite);
text-align: left;
text-align: center;
font-weight: bold;
font-size: 15px;
opacity: 0;
+2 -1
View File
@@ -10,7 +10,8 @@ interface CssExports {
'externalLinks': string;
'link': string;
'monitorToggleButton': string;
'overlay': string;
'overlayHover': string;
'overlayHoverTitle': string;
'overlayTitle': string;
'poster': string;
'posterContainer': string;
@@ -82,6 +82,7 @@ class CollectionMovie extends Component {
} = this.props;
const {
hasPosterError,
isEditMovieModalOpen,
isNewAddMovieModalOpen
} = this.state;
@@ -134,26 +135,31 @@ class CollectionMovie extends Component {
onLoad={this.onPosterLoad}
/>
<div className={styles.overlay}>
<div className={styles.overlayTitle}>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{title}
</div>
}
<div className={styles.overlayHover}>
<div className={styles.overlayHoverTitle}>
{title} {year > 0 ? `(${year})` : ''}
</div>
{
id ?
<div className={styles.overlayStatus}>
<MovieIndexProgressBar
movieId={id}
movieFile={movieFile}
monitored={monitored}
hasFile={hasFile}
status={status}
bottomRadius={true}
width={posterWidth}
detailedProgressBar={detailedProgressBar}
isAvailable={isAvailable}
/>
</div> :
<MovieIndexProgressBar
movieId={id}
movieFile={movieFile}
monitored={monitored}
hasFile={hasFile}
status={status}
bottomRadius={true}
width={posterWidth}
detailedProgressBar={detailedProgressBar}
isAvailable={isAvailable}
/> :
null
}
</div>
@@ -196,7 +196,7 @@ class CollectionOverview extends Component {
size={13}
/>
<span className={styles.status}>
{`${missingMovies} missing movie(s)`}
{translate('CountMissingMoviesFromLibrary', { count: missingMovies })}
</span>
</Label>
@@ -14,17 +14,17 @@ const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
function calculatePosterWidth(posterSize, isSmallScreen) {
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
const maximumPosterWidth = isSmallScreen ? 152 : 162;
if (posterSize === 'large') {
return maxiumPosterWidth;
return maximumPosterWidth;
}
if (posterSize === 'medium') {
return Math.floor(maxiumPosterWidth * 0.75);
return Math.floor(maximumPosterWidth * 0.75);
}
return Math.floor(maxiumPosterWidth * 0.5);
return Math.floor(maximumPosterWidth * 0.5);
}
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
@@ -92,15 +92,14 @@ class CollectionOverviews extends Component {
if (this._grid && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
this._gridScrollToPosition({ scrollTop });
}
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (this._grid && index != null) {
this._grid.scrollToCell({
this._gridScrollToCell({
rowIndex: index,
columnIndex: 0
});
@@ -186,6 +185,19 @@ class CollectionOverviews extends Component {
);
};
_gridScrollToCell = ({ rowIndex = 0, columnIndex = 0 }) => {
const scrollOffset = this._grid.getOffsetForCell({
rowIndex,
columnIndex
});
this._gridScrollToPosition(scrollOffset);
};
_gridScrollToPosition = ({ scrollTop = 0, scrollLeft = 0 }) => {
this.props.scroller?.scrollTo({ top: scrollTop, left: scrollLeft });
};
//
// Listeners
@@ -56,6 +56,8 @@ function CustomFiltersModalContent(props) {
{translate('AddCustomFilter')}
</Button>
</div>
<br />
{translate('FilterMoviePropertiesOnlyNotFileWarning')}
</ModalBody>
<ModalFooter>
+3 -1
View File
@@ -5,18 +5,20 @@ import { ValidationError, ValidationWarning } from 'typings/pending';
import styles from './Form.css';
export interface FormProps {
id?: string;
children: ReactNode;
validationErrors?: ValidationError[];
validationWarnings?: ValidationWarning[];
}
function Form({
id,
children,
validationErrors = [],
validationWarnings = [],
}: FormProps) {
return (
<div>
<div id={id}>
{validationErrors.length || validationWarnings.length ? (
<div className={styles.validationFailures}>
{validationErrors.map((error, index) => {
@@ -8,12 +8,14 @@ import styles from './FormInputButton.css';
export interface FormInputButtonProps extends ButtonProps {
canSpin?: boolean;
isLastButton?: boolean;
isSpinning?: boolean;
}
function FormInputButton({
className = styles.button,
canSpin = false,
isLastButton = true,
isSpinning = false,
kind = kinds.PRIMARY,
...otherProps
}: FormInputButtonProps) {
@@ -22,6 +24,7 @@ function FormInputButton({
<SpinnerButton
className={classNames(className, !isLastButton && styles.middleButton)}
kind={kind}
isSpinning={isSpinning}
{...otherProps}
/>
);
@@ -13,11 +13,11 @@ import { Manager, Popper, Reference } from 'react-popper';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import Portal from 'Components/Portal';
import Scroller from 'Components/Scroller/Scroller';
import useMeasure from 'Helpers/Hooks/useMeasure';
import { icons } from 'Helpers/Props';
import ArrayElement from 'typings/Helpers/ArrayElement';
import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs';
@@ -162,13 +162,13 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
onOpen,
} = props;
const [measureRef, { width }] = useMeasure();
const updater = useRef<(() => void) | null>(null);
const buttonId = useMemo(() => getUniqueElementId(), []);
const optionsId = useMemo(() => getUniqueElementId(), []);
const [selectedIndex, setSelectedIndex] = useState(
getSelectedIndex(value, values)
);
const [width, setWidth] = useState(0);
const [isOpen, setIsOpen] = useState(false);
const isMobile = useMemo(() => isMobileUtil(), []);
@@ -378,13 +378,6 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
]
);
const handleMeasure = useCallback(
({ width: newWidth }: { width: number }) => {
setWidth(newWidth);
},
[setWidth]
);
const handleOptionsModalClose = useCallback(() => {
setIsOpen(false);
}, [setIsOpen]);
@@ -418,7 +411,7 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
<Reference>
{({ ref }) => (
<div ref={ref} id={buttonId}>
<Measure whitelist={['width']} onMeasure={handleMeasure}>
<div ref={measureRef}>
{isEditable && typeof value === 'string' ? (
<div className={styles.editableContainer}>
<TextInput
@@ -492,7 +485,7 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
</div>
</Link>
)}
</Measure>
</div>
</div>
)}
</Reference>
@@ -18,7 +18,7 @@ function createQualityProfilesSelector(
includeMixed: boolean
) {
return createSelector(
createSortedSectionSelector(
createSortedSectionSelector<QualityProfile, QualityProfilesAppState>(
'settings.qualityProfiles',
sortByProp<QualityProfile, 'name'>('name')
),
@@ -88,13 +88,10 @@ function QualityProfileSelectInput({
);
const handleChange = useCallback(
({ value: newValue }: EnhancedSelectInputChanged<string | number>) => {
onChange({
name,
value: newValue === 'noChange' ? value : newValue,
});
({ value }: EnhancedSelectInputChanged<string | number>) => {
onChange({ name, value });
},
[name, value, onChange]
[name, onChange]
);
useEffect(() => {
@@ -14,7 +14,7 @@ import {
RenderSuggestion,
SuggestionsFetchRequestedParams,
} from 'react-autosuggest';
import useDebouncedCallback from 'Helpers/Hooks/useDebouncedCallback';
import { useDebouncedCallback } from 'use-debounce';
import { Kind } from 'Helpers/Props/kinds';
import { InputChanged } from 'typings/inputs';
import AutoSuggestInput from '../AutoSuggestInput';
+4
View File
@@ -26,6 +26,10 @@
color: var(--warningColor);
}
.primary {
color: var(--primaryColor);
}
.purple {
color: var(--purple);
}
+1
View File
@@ -6,6 +6,7 @@ interface CssExports {
'disabled': string;
'info': string;
'pink': string;
'primary': string;
'purple': string;
'success': string;
'warning': string;
-43
View File
@@ -1,43 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import { kinds, sizes } from 'Helpers/Props';
import Label from './Label';
import styles from './ImportListList.css';
function ImportListList({ lists, importListList }) {
return (
<div className={styles.lists}>
{
lists.map((t) => {
const list = _.find(importListList, { id: t });
if (!list) {
return null;
}
return (
<Label
key={list.id}
kind={kinds.SUCCESS}
size={sizes.MEDIUM}
>
{list.name}
</Label>
);
})
}
</div>
);
}
ImportListList.propTypes = {
lists: PropTypes.arrayOf(PropTypes.number).isRequired,
importListList: PropTypes.arrayOf(PropTypes.object).isRequired
};
ImportListList.defaultProps = {
lists: []
};
export default ImportListList;
@@ -0,0 +1,35 @@
import React from 'react';
import { useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import Label from './Label';
import styles from './ImportListList.css';
interface ImportListListProps {
lists: number[];
}
function ImportListList({ lists }: ImportListListProps) {
const allImportLists = useSelector(
(state: AppState) => state.settings.importLists.items
);
return (
<div className={styles.lists}>
{lists.map((id) => {
const importList = allImportLists.find((list) => list.id === id);
if (!importList) {
return null;
}
return (
<Label key={importList.id} kind="success" size="medium">
{importList.name}
</Label>
);
})}
</div>
);
}
export default ImportListList;
@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createImportListSelector from 'Store/Selectors/createImportListSelector';
import ImportListList from './ImportListList';
function createMapStateToProps() {
return createSelector(
createImportListSelector(),
(importListList) => {
return {
importListList
};
}
);
}
export default connect(createMapStateToProps)(ImportListList);
+3 -5
View File
@@ -1,16 +1,14 @@
import classNames from 'classnames';
import React from 'react';
import { align, kinds, sizes } from 'Helpers/Props';
import { kinds, sizes } from 'Helpers/Props';
import { Align } from 'Helpers/Props/align';
import { Kind } from 'Helpers/Props/kinds';
import { Size } from 'Helpers/Props/sizes';
import Link, { LinkProps } from './Link';
import styles from './Button.css';
export interface ButtonProps extends Omit<LinkProps, 'children' | 'size'> {
buttonGroupPosition?: Extract<
(typeof align.all)[number],
keyof typeof styles
>;
buttonGroupPosition?: Extract<Align, keyof typeof styles>;
kind?: Extract<Kind, keyof typeof styles>;
size?: Extract<Size, keyof typeof styles>;
children: Required<LinkProps['children']>;
@@ -1,58 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props';
import Button from './Button';
import styles from './SpinnerButton.css';
function SpinnerButton(props) {
const {
className,
isSpinning,
isDisabled,
spinnerIcon,
children,
...otherProps
} = props;
return (
<Button
className={classNames(
className,
styles.button,
isSpinning && styles.isSpinning
)}
isDisabled={isDisabled || isSpinning}
{...otherProps}
>
<span className={styles.spinnerContainer}>
<Icon
className={styles.spinner}
name={spinnerIcon}
isSpinning={true}
/>
</span>
<span className={styles.label}>
{children}
</span>
</Button>
);
}
SpinnerButton.propTypes = {
...Button.Props,
className: PropTypes.string.isRequired,
isSpinning: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool,
spinnerIcon: PropTypes.object.isRequired,
children: PropTypes.node
};
SpinnerButton.defaultProps = {
className: styles.button,
spinnerIcon: icons.SPINNER
};
export default SpinnerButton;

Some files were not shown because too many files have changed in this diff Show More