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

Compare commits

...

107 Commits

Author SHA1 Message Date
Bogdan cd29c0c9c8 Fix stable branch label in update 2024-10-20 04:41:20 +03:00
Servarr 9986d04d36 Automated API Docs update 2024-10-19 09:48:14 +03:00
Mark McDowall f900d623dc New: Allow major version updates to be installed
(cherry picked from commit 0e95ba2021b23cc65bce0a0620dd48e355250dab)
2024-10-19 09:01:56 +03:00
Bogdan 84b507faf3 New: Romania and India added to list of Certification Countries 2024-10-19 08:59:29 +03:00
Bogdan adb27123df Natural sorting for tags list in the UI
(cherry picked from commit 09d3ae969281715a26ffd374c148cafe17a8f438)
2024-10-18 23:04:31 +03:00
Bogdan a06792b923 New: Sync updates to UI for providers (#10550) 2024-10-18 23:02:42 +03:00
Bogdan d90ee3ae11 Fixed: Release Year mandatory to generate valid file formats 2024-10-18 12:48:51 +03:00
Bogdan ff38afd198 Fixed: Add only movies with release dates from monitored collections 2024-10-18 12:09:37 +03:00
Weblate db70c06b8b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Buloni <ershov.artjom@yandex.ru>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: JoseFilipeFerreira <jose.filipe.matos.ferreira@gmail.com>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nb_NO/
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/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/sk/
Translation: Servarr/Radarr
2024-10-16 04:07:30 +03:00
costaht fb7656be56 New: Parse pt-BR releases as Brazilian Portuguese (#10554)
* New: Match releases with pt-BR (BPC-47) with Portuguese Brazilian
2024-10-15 05:59:42 +03:00
Bogdan 3287e7cdec Bump dotnet to 6.0.35 2024-10-13 19:42:07 +03:00
Bogdan 0761e27cfa New: Parse ES as Spanish 2024-10-12 19:54:54 +03:00
Bogdan 4f47bb39ac Bump version to 5.13.0 2024-10-12 18:58:59 +03:00
Bogdan 889d071004 New: Display items tags on import lists index 2024-10-10 22:51:32 +03:00
Bogdan 0049922ab6 Include exception message in SkyHook failure message 2024-10-10 21:17:20 +03:00
Bogdan 3c995a0fff Bump babel packages 2024-10-10 19:01:53 +03:00
Bogdan 430719baac Remove unused gulp packages 2024-10-10 18:55:46 +03:00
Bogdan 9928d711a3 Trim multiple occurrences of ending separators in filename 2024-10-10 15:26:00 +03:00
Bogdan f90b43b3e1 Simplify parsing IMDb and TMDb urls as search terms 2024-10-10 03:25:10 +03:00
Steel City Phantom 64122b4cfb Auto-detect building on macOS ARM (#10539) 2024-10-10 02:41:07 +03:00
Bogdan 7912a942f7 Bump frontend packages 2024-10-10 02:40:02 +03:00
Bogdan 0a7607bb62 Bump dotnet packages 2024-10-10 02:40:02 +03:00
Vincent Caron beeb5204b8 New: Parse IMDB and TMDB URLs as search terms 2024-10-10 00:32:39 +01:00
Bogdan ab13fb6e99 Fix index variable in fuse worker 2024-10-09 01:26:26 +03:00
Weblate 2a3d595a66 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Ardenet <1213193613@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: angelsky11 <angelsky11@gmail.com>
Co-authored-by: anne <gagatebis@hotmail.com>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: jsain <josip.sain@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ar/
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/he/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/hi/
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/is/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/nl/
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/th/
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/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-10-08 13:26:52 +03:00
Jared Ledvina 958a863d8f Recompare file size after import file if necessary
(cherry picked from commit 6660db22ecf53d7747e3abc400529669ea779fa1)
2024-10-08 13:25:43 +03:00
Servarr 8b7884deb0 Automated API Docs update 2024-10-08 13:25:15 +03:00
Bogdan 9a22e1c791 Bump browserslist-db 2024-10-08 02:15:07 +03:00
Bogdan f0f828491b Fixed: Copy to clipboard in non-secure contexts
(cherry picked from commit 3828e475cc8860e74cdfd8a70b4f886de7f9c5c3)

Closes #10525
2024-10-08 02:11:35 +03:00
Treycos 7f3d107eda Convert ClipboardButton to TypeScript
(cherry picked from commit 99fc52039f44264c83d939e5f096d8e16d2f3355)

Closes #10452
2024-10-08 02:09:10 +03:00
Bogdan ce4477eeac Improve filename examples for movies naming 2024-10-08 01:54:46 +03:00
Bogdan 8b64f873f4 Convert Naming options to TypeScript 2024-10-08 01:54:46 +03:00
Bogdan 38bd060960 Convert FormInputButton to TypeScript 2024-10-08 01:54:46 +03:00
Bogdan 7c243cb6e8 Fixed: Error updating providers with ID missing from JSON
(cherry picked from commit c435fcd685cc97e98d14f747227eefd39e4d1164)
2024-10-08 01:52:26 +03:00
Bogdan b29dee63f4 Use the first allowed quality for cutoff met rejection message with disabled upgrades 2024-10-07 22:26:55 +03:00
Mark McDowall f6542bab0a New: Use 307 redirect for requests missing URL Base 2024-10-06 17:22:32 +03:00
Bogdan da1b53b7e2 Bump macOS runner version to 13 2024-10-06 16:21:38 +03:00
Bogdan 0deae95782 Bump version to 5.12.2 2024-10-06 12:03:04 +03:00
Mark McDowall 75c7a3cfc6 Fixed: Ignore free space check before grabbing if directory is missing 2024-10-06 00:15:40 +03:00
Bogdan cfdb7a15de Simplify defaults set when adding release profiles and list exclusions 2024-10-05 13:02:36 +03:00
Bogdan 63a7d33e7e Fixed: Cleaning the path for movie collections with top level folders 2024-10-05 12:01:13 +03:00
Bogdan c9836f997c Fixed: Clean paths for top level root folders 2024-10-05 12:01:13 +03:00
Bogdan d37e71415f Convert Release Profiles to TypeScript 2024-10-04 17:28:04 +03:00
Bogdan 9a5f4bef63 Check if root folder is not empty on files import 2024-10-04 12:04:09 +03:00
Bogdan 40551ba5a3 Fixed: Custom filters with release date filter
Fixes #10508
2024-10-02 22:32:33 +03:00
Bogdan 6e04dc894b Fixed: Validate path on movie update 2024-10-02 19:27:07 +03:00
Bogdan ac767ed386 New: Add 'Movie CleanTitleThe' token
First attempt to fix movie folder validation by ignoring invalid tokens mixed from 'Original' and 'The'

Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
2024-10-02 19:23:57 +03:00
Mark McDowall 42fbb79017 New: Parse 'BEN THE MAN' release group
(cherry picked from commit da610a1f409c9c03cbed1c27ccaedc32f42e636c)
2024-10-02 15:39:41 +03:00
Bogdan c43bd77dae Display long date tooltips for release dates 2024-10-02 10:12:07 +03:00
Lorenzo Lewis 68dfa55b35 Fix typo README.md (#10502) 2024-10-01 12:04:50 -05:00
Bogdan fa190c85a3 Add new category for FL 2024-09-30 17:14:18 +03:00
Bogdan 172dcf6f8d Bump version to 5.12.1 2024-09-29 08:16:01 +03:00
Weblate 0736fc955f Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Jhonata da Rocha <jhonata182@gmail.com>
Co-authored-by: Lizandra Candido da Silva <lizandra.c.s@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
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/hu/
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/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/radarr/zh_CN/
Translation: Servarr/Radarr
2024-09-28 04:22:34 +03:00
Bogdan 9d0b8d974d Fixed: Parsing of Hybrid-Remux 2024-09-28 04:21:08 +03:00
Bogdan 9a3e89f283 Fixed: Ignore '.DS_Store' and '.unmanic' when scanning for files 2024-09-28 04:20:22 +03:00
Mark McDowall e33e45ec73 Fixed: Don't reject revision upgrades if profile doesn't allow upgrades
(cherry picked from commit 4f0e1c54c167f5123a33d19b76653450401adb6d)
2024-09-28 04:19:42 +03:00
Mark McDowall 5893d88058 Fixed: Ignore extra spaces in path when not running on Windows
(cherry picked from commit 6d0f10b877912edef21232c64339cc6548d9690e)
2024-09-28 04:18:47 +03:00
Servarr a81d27acda Automated API Docs update 2024-09-26 11:43:29 +03:00
Mark McDowall d2b279a6be Fixed: Replace illegal characters even when renaming is disabled
(cherry picked from commit 4d8a4436810828494e99f0854cf6de3269668fe4)
2024-09-26 10:22:40 +03:00
Bogdan 6686fa0600 New: Smart as default Colon Replacement
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-09-26 10:22:40 +03:00
Bogdan 1d286df85d Display naming example errors when all fields are empty 2024-09-26 10:22:40 +03:00
yammes08 be2e1e4fdb Fixed: SDR files being parsed as HLG
(cherry picked from commit 11e5c5a11b171138c235224c1aa9a258f0a4ec4d)
2024-09-25 09:41:21 +03:00
Bogdan 08868e5d01 Bump version to 5.12.0 2024-09-25 09:39:42 +03:00
Mark McDowall 7b43c2e345 Fixed: Loading movie images after placeholder in Safari
Closes #10474
2024-09-25 06:48:30 +03:00
Bogdan dc599b6531 Sort allowed sorting keys 2024-09-25 06:47:37 +03:00
Weblate 1421179654 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: FloatStream <1213193613@qq.com>
Co-authored-by: GkhnGRBZ <gkhn.gurbuz@hotmail.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: fordas <fordas15@gmail.com>
Co-authored-by: liuwqq <843384478@qq.com>
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/it/
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/zh_CN/
Translation: Servarr/Radarr
2024-09-25 02:35:29 +03:00
Bogdan 41dcf32e24 Fix translations for MovieMonitoredSelectInput 2024-09-25 02:33:02 +03:00
Bogdan 7a813a44b6 Use UTC to calculate movie status 2024-09-25 02:33:02 +03:00
Bogdan 54a5059080 Convert AvailabilitySelectInput to TypeScript 2024-09-25 02:33:02 +03:00
Bogdan adaf7444d3 Add movie availability descriptions 2024-09-25 02:33:02 +03:00
Robin Dadswell 49d11e59b3 Fixed: Telegram Log Message 2024-09-24 16:59:30 +01:00
Servarr a7eb4a4a04 Automated API Docs update 2024-09-24 12:10:31 +03:00
Bogdan 66a6a663ba Prevent line wraps on mobile for ratings 2024-09-24 03:31:43 +03:00
Bogdan f735e31835 New: Trakt ratings 2024-09-24 03:31:43 +03:00
Bogdan b8f1286abb Fixed: Sorting queue by columns 2024-09-22 07:30:46 +03:00
Mark McDowall 9df45199d0 Reprocessing manual import items unable to detect sample
(cherry picked from commit 27da0413882dc87e1617a5d091ac5111589e61a6)

Closes #10463
2024-09-22 05:21:38 +03:00
Servarr a692c35b03 Automated API Docs update 2024-09-21 23:42:37 +03:00
momo ddcad270c3 Fix description for API key as query parameter
(cherry picked from commit 30c36fdc3baa686102ff124833c7963fc786f251)
2024-09-21 21:18:15 +03:00
Bogdan b06f1d7c12 Bump version to 5.11.0 2024-09-21 03:57:59 +03:00
Mark McDowall 480bb50b85 Fixed: Rejections for Custom Format score increment 2024-09-21 02:17:18 +03:00
Bogdan dbc94dbe4e Simplify fallback to default for allowed sort keys
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
2024-09-21 01:35:47 +03:00
Mark McDowall b89271fc01 Fixed: Unable to login when instance name contained brackets 2024-09-21 01:23:25 +03:00
Bogdan 66fcde7325 Include current quality in rejection message for not an upgrade 2024-09-21 00:41:26 +03:00
Bogdan 463741da1f New: Fetch up to 1000 movies from Plex Watchlist 2024-09-18 03:49:49 +03:00
Bogdan 3388fae1a5 Fix translation key for Skip Free Space Check help text 2024-09-17 17:46:33 +03:00
bakerboy448 72b2cfe8be Fixed: Parse TELESYNCH as TELESYNC (#10445)
Fixes #10414
2024-09-17 02:34:12 +03:00
Servarr d5dd5e08ca Automated API Docs update 2024-09-17 01:22:25 +03:00
Bogdan fabd40cbae New: Allowed sort keys for paginated resources 2024-09-16 20:27:34 +03:00
Servarr 3ca327f611 Multiple Translations updated by Weblate (#10418)
ignore-downstream






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_CN/
Translation: Servarr/Radarr

Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Kuzmich55 <kuzmich55@gmail.com>
Co-authored-by: genoher <genoher@gmail.com>
Co-authored-by: rookie7420 <yuanchong2001@qq.com>
Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-09-16 18:34:20 +03:00
Bogdan c804140896 Fix use of min score increment in rejection message 2024-09-16 18:19:02 +03:00
somniumV bb43d0c796 New: Minimum Upgrade Score for Custom Formats
(cherry picked from commit 8b20a9449c1ae5ffd1e8d12f1ca771727b8c52a5)
2024-09-16 18:19:02 +03:00
Mark McDowall 5757fa797f New: Use instance name in forms authentication cookie name
Closes #10416
2024-09-16 01:29:10 +03:00
Bogdan 2fc32189d8 Convert Movie Titles to TypeScript 2024-09-16 01:18:23 +03:00
Bogdan 5975be3690 Fixed: Removing import lists for cast and crew from movie details
Convert movie credits to TypeScript

Switching to metadata based order for crew
2024-09-16 01:18:23 +03:00
amdavie 6095819005 New: Scene and Nuked IndexerFlags for Newznab indexers
(cherry picked from commit 278c7891a3add639b4ff5bc1f4f5e8912dabc897)
2024-09-15 23:08:20 +03:00
Bogdan 7528882adf Gotify notification updates
New: Option to include links for Gotify notifications
New: Include images and links for Android

(cherry picked from commit 3c857135c59029635b0972f959f9a8255bcff21f)

Closes #10433
Fixes #10410
2024-09-15 22:42:59 +03:00
Mark McDowall c1f1307345 New: Add exception to SSL Certificate validation message
(cherry picked from commit d84c4500949a530fac92d73f7f2f8e8462b37244)

Closes #10437
2024-09-15 22:42:59 +03:00
Mark McDowall 348060351a New: Check for available space before grabbing
(cherry picked from commit 4b5ff3927d3c123f9e3a2bc74328323fab1b0745)

Closes #10429
2024-09-15 22:42:59 +03:00
Mark McDowall ca31cdd33a New: Add additional archive exentions
(cherry picked from commit 750a9353f82da4e016bee25e0c625cd6d8613b57)
2024-09-15 20:41:48 +03:00
Bogdan 36e278aa82 Bump version to 5.10.4 2024-09-15 15:52:54 +03:00
Bogdan 927e84654f Fixed: Filtering by IMDb decimal ratings 2024-09-13 01:25:37 +03:00
Bogdan 96e60906c5 Fixed: Empty or private MDBList lists shown as valid on save 2024-09-12 19:36:04 +03:00
Bogdan 7a55b563c0 New: Importing sup files as subtitles
Towards #10412
2024-09-11 20:44:38 +03:00
Servarr b4bbb71a9b Automated API Docs update 2024-09-09 20:53:17 +03:00
ManiMatter 0361299a73 New: Last Searched column on Wanted screens (#10392)
* Adding lastSearchTime to API and "Last Searched" to Frontend (cutoff unmet & missing)
Picking lastSearchTime from movie instead of movieMetaData

---------

Co-authored-by: Bogdan <mynameisbogdan@users.noreply.github.com>
2024-09-08 20:56:11 +03:00
Bogdan e11339fb83 Fix weblate widget 2024-09-08 11:26:02 +03:00
Bogdan fbdd3129f5 Bump version to 5.10.3 2024-09-08 11:12:25 +03:00
289 changed files with 6922 additions and 5657 deletions
+2 -2
View File
@@ -1,7 +1,7 @@
# Radarr # Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop) [![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget) [![Translation status](https://translate.servarr.com/widget/servarr/radarr/svg-badge.svg)](https://translate.servarr.com/engage/servarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker) [![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation/docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg) ![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers) [![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
@@ -9,7 +9,7 @@
[![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors) [![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors)
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available. Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances. Note that only one type of a given movie is supported. If you want both a 4k version and 1080p version of a given movie you will need multiple instances.
## Major Features Include ## Major Features Include
+3 -3
View File
@@ -9,18 +9,18 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.10.2' majorVersion: '5.13.0'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.424' dotnetVersion: '6.0.427'
nodeVersion: '20.X' nodeVersion: '20.X'
innoVersion: '6.2.2' innoVersion: '6.2.2'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-20.04'
macImage: 'macOS-12' macImage: 'macOS-13'
trigger: trigger:
branches: branches:
@@ -131,7 +131,9 @@ class AddNewMovie extends Component {
<div className={styles.helpText}> <div className={styles.helpText}>
{translate('FailedLoadingSearchResults')} {translate('FailedLoadingSearchResults')}
</div> </div>
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
<Alert kind={kinds.DANGER}>{getErrorMessage(error)}</Alert>
<div> <div>
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb"> <Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
{translate('WhySearchesCouldBeFailing')} {translate('WhySearchesCouldBeFailing')}
@@ -1,16 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import CheckInput from 'Components/Form/CheckInput'; import CheckInput from 'Components/Form/CheckInput';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AddNewMovieModalContent.css'; import styles from './AddNewMovieModalContent.css';
@@ -115,13 +118,28 @@ class AddNewMovieModalContent extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel> <FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.AVAILABILITY_SELECT} type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability" name="minimumAvailability"
onChange={onInputChange} onChange={onInputChange}
{...minimumAvailability} {...minimumAvailability}
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
/> />
</FormGroup> </FormGroup>
@@ -1,8 +1,12 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import Icon from 'Components/Icon';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell'; import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './ImportMovieHeader.css'; import styles from './ImportMovieHeader.css';
@@ -46,7 +50,19 @@ function ImportMovieHeader(props) {
className={styles.minimumAvailability} className={styles.minimumAvailability}
name="minimumAvailability" name="minimumAvailability"
> >
{translate('MinAvailability')} {translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.detailsIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.LEFT}
/>
</VirtualTableHeaderCell> </VirtualTableHeaderCell>
<VirtualTableHeaderCell <VirtualTableHeaderCell
@@ -0,0 +1,27 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function MovieMinimumAvailabilityPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title={translate('Announced')}
data={translate('AnnouncedMovieAvailabilityDescription')}
/>
<DescriptionListItem
title={translate('InCinemas')}
data={translate('InCinemasMovieAvailabilityDescription')}
/>
<DescriptionListItem
title={translate('Released')}
data={translate('ReleasedMovieAvailabilityDescription')}
/>
</DescriptionList>
);
}
export default MovieMinimumAvailabilityPopoverContent;
+2 -2
View File
@@ -31,7 +31,7 @@ import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs'; import Logs from 'System/Logs/Logs';
import Status from 'System/Status/Status'; import Status from 'System/Status/Status';
import Tasks from 'System/Tasks/Tasks'; import Tasks from 'System/Tasks/Tasks';
import UpdatesConnector from 'System/Updates/UpdatesConnector'; import Updates from 'System/Updates/Updates';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector'; import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector'; import MissingConnector from 'Wanted/Missing/MissingConnector';
@@ -228,7 +228,7 @@ function AppRoutes(props) {
<Route <Route
path="/system/updates" path="/system/updates"
component={UpdatesConnector} component={Updates}
/> />
<Route <Route
+3 -1
View File
@@ -1,9 +1,10 @@
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
import BlocklistAppState from './BlocklistAppState'; import BlocklistAppState from './BlocklistAppState';
import CalendarAppState from './CalendarAppState'; import CalendarAppState from './CalendarAppState';
import CommandAppState from './CommandAppState'; import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState'; import HistoryAppState from './HistoryAppState';
import InteractiveImportAppState from './InteractiveImportAppState';
import MovieCollectionAppState from './MovieCollectionAppState'; import MovieCollectionAppState from './MovieCollectionAppState';
import MovieCreditAppState from './MovieCreditAppState';
import MovieFilesAppState from './MovieFilesAppState'; import MovieFilesAppState from './MovieFilesAppState';
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState'; import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
import ParseAppState from './ParseAppState'; import ParseAppState from './ParseAppState';
@@ -64,6 +65,7 @@ interface AppState {
history: HistoryAppState; history: HistoryAppState;
interactiveImport: InteractiveImportAppState; interactiveImport: InteractiveImportAppState;
movieCollections: MovieCollectionAppState; movieCollections: MovieCollectionAppState;
movieCredits: MovieCreditAppState;
movieFiles: MovieFilesAppState; movieFiles: MovieFilesAppState;
movieIndex: MovieIndexAppState; movieIndex: MovieIndexAppState;
movies: MoviesAppState; movies: MoviesAppState;
@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import MovieCredit from 'typings/MovieCredit';
interface MovieCreditAppState extends AppSectionState<MovieCredit> {}
export default MovieCreditAppState;
+1
View File
@@ -27,6 +27,7 @@ export interface MovieIndexAppState {
showTmdbRating: boolean; showTmdbRating: boolean;
showImdbRating: boolean; showImdbRating: boolean;
showRottenTomatoesRating: boolean; showRottenTomatoesRating: boolean;
showTraktRating: boolean;
showTags: boolean; showTags: boolean;
showSearchAction: boolean; showSearchAction: boolean;
}; };
@@ -16,6 +16,9 @@ import IndexerFlag from 'typings/IndexerFlag';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import QualityProfile from 'typings/QualityProfile'; import QualityProfile from 'typings/QualityProfile';
import General from 'typings/Settings/General'; import General from 'typings/Settings/General';
import NamingConfig from 'typings/Settings/NamingConfig';
import NamingExample from 'typings/Settings/NamingExample';
import ReleaseProfile from 'typings/Settings/ReleaseProfile';
import UiSettings from 'typings/Settings/UiSettings'; import UiSettings from 'typings/Settings/UiSettings';
export interface DownloadClientAppState export interface DownloadClientAppState
@@ -29,6 +32,13 @@ export interface GeneralAppState
extends AppSectionItemState<General>, extends AppSectionItemState<General>,
AppSectionSaveState {} AppSectionSaveState {}
export interface NamingAppState
extends AppSectionItemState<NamingConfig>,
AppSectionSaveState {}
export interface NamingExamplesAppState
extends AppSectionItemState<NamingExample> {}
export interface ImportListAppState export interface ImportListAppState
extends AppSectionState<ImportList>, extends AppSectionState<ImportList>,
AppSectionDeleteState, AppSectionDeleteState,
@@ -49,6 +59,12 @@ export interface QualityProfilesAppState
extends AppSectionState<QualityProfile>, extends AppSectionState<QualityProfile>,
AppSectionSchemaState<QualityProfile> {} AppSectionSchemaState<QualityProfile> {}
export interface ReleaseProfilesAppState
extends AppSectionState<ReleaseProfile>,
AppSectionSaveState {
pendingChanges: Partial<ReleaseProfile>;
}
export interface CustomFormatAppState export interface CustomFormatAppState
extends AppSectionState<CustomFormat>, extends AppSectionState<CustomFormat>,
AppSectionDeleteState, AppSectionDeleteState,
@@ -81,8 +97,11 @@ interface SettingsAppState {
indexerFlags: IndexerFlagSettingsAppState; indexerFlags: IndexerFlagSettingsAppState;
indexers: IndexerAppState; indexers: IndexerAppState;
languages: LanguageSettingsAppState; languages: LanguageSettingsAppState;
naming: NamingAppState;
namingExamples: NamingExamplesAppState;
notifications: NotificationAppState; notifications: NotificationAppState;
qualityProfiles: QualityProfilesAppState; qualityProfiles: QualityProfilesAppState;
releaseProfiles: ReleaseProfilesAppState;
ui: UiSettingsAppState; ui: UiSettingsAppState;
} }
+3
View File
@@ -2,18 +2,21 @@ import DiskSpace from 'typings/DiskSpace';
import Health from 'typings/Health'; import Health from 'typings/Health';
import SystemStatus from 'typings/SystemStatus'; import SystemStatus from 'typings/SystemStatus';
import Task from 'typings/Task'; import Task from 'typings/Task';
import Update from 'typings/Update';
import AppSectionState, { AppSectionItemState } from './AppSectionState'; import AppSectionState, { AppSectionItemState } from './AppSectionState';
export type DiskSpaceAppState = AppSectionState<DiskSpace>; export type DiskSpaceAppState = AppSectionState<DiskSpace>;
export type HealthAppState = AppSectionState<Health>; export type HealthAppState = AppSectionState<Health>;
export type SystemStatusAppState = AppSectionItemState<SystemStatus>; export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
export type TaskAppState = AppSectionState<Task>; export type TaskAppState = AppSectionState<Task>;
export type UpdateAppState = AppSectionState<Update>;
interface SystemAppState { interface SystemAppState {
diskSpace: DiskSpaceAppState; diskSpace: DiskSpaceAppState;
health: HealthAppState; health: HealthAppState;
status: SystemStatusAppState; status: SystemStatusAppState;
tasks: TaskAppState; tasks: TaskAppState;
updates: UpdateAppState;
} }
export default SystemAppState; export default SystemAppState;
@@ -56,7 +56,9 @@ function getValue(input, selectedFilterBuilderProp) {
} }
if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) { if (selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER) {
return parseInt(input); const { numberFractionDigits = 0 } = selectedFilterBuilderProp;
return Number(input).toFixed(numberFractionDigits);
} }
return input; return input;
@@ -1,69 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
const availabilityOptions = [
{
key: 'announced',
get value() {
return translate('Announced');
}
},
{
key: 'inCinemas',
get value() {
return translate('InCinemas');
}
},
{
key: 'released',
get value() {
return translate('Released');
}
}
];
function AvailabilitySelectInput(props) {
const values = [...availabilityOptions];
const {
includeNoChange,
includeMixed
} = props;
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
isDisabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
isDisabled: true
});
}
return (
<EnhancedSelectInput
{...props}
values={values}
/>
);
}
AvailabilitySelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired
};
AvailabilitySelectInput.defaultProps = {
includeNoChange: false,
includeMixed: false
};
export default AvailabilitySelectInput;
@@ -0,0 +1,67 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput';
interface AvailabilitySelectInputProps {
includeNoChange: boolean;
includeNoChangeDisabled?: boolean;
includeMixed?: boolean;
}
interface IMovieAvailabilityOption {
key: string;
value: string;
format?: string;
isDisabled?: boolean;
}
const movieAvailabilityOptions: IMovieAvailabilityOption[] = [
{
key: 'announced',
get value() {
return translate('Announced');
},
},
{
key: 'inCinemas',
get value() {
return translate('InCinemas');
},
},
{
key: 'released',
get value() {
return translate('Released');
},
},
];
function AvailabilitySelectInput(props: AvailabilitySelectInputProps) {
const values = [...movieAvailabilityOptions];
const {
includeNoChange = false,
includeNoChangeDisabled = true,
includeMixed = false,
} = props;
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: translate('NoChange'),
isDisabled: includeNoChangeDisabled,
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: `(${translate('Mixed')})`,
isDisabled: true,
});
}
return <EnhancedSelectInput {...props} values={values} />;
}
export default AvailabilitySelectInput;
@@ -1,54 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import { kinds } from 'Helpers/Props';
import styles from './FormInputButton.css';
function FormInputButton(props) {
const {
className,
canSpin,
isLastButton,
...otherProps
} = props;
if (canSpin) {
return (
<SpinnerButton
className={classNames(
className,
!isLastButton && styles.middleButton
)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
return (
<Button
className={classNames(
className,
!isLastButton && styles.middleButton
)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
FormInputButton.propTypes = {
className: PropTypes.string.isRequired,
isLastButton: PropTypes.bool.isRequired,
canSpin: PropTypes.bool.isRequired
};
FormInputButton.defaultProps = {
className: styles.button,
isLastButton: true,
canSpin: false
};
export default FormInputButton;
@@ -0,0 +1,38 @@
import classNames from 'classnames';
import React from 'react';
import Button, { ButtonProps } from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import { kinds } from 'Helpers/Props';
import styles from './FormInputButton.css';
export interface FormInputButtonProps extends ButtonProps {
canSpin?: boolean;
isLastButton?: boolean;
}
function FormInputButton({
className = styles.button,
canSpin = false,
isLastButton = true,
...otherProps
}: FormInputButtonProps) {
if (canSpin) {
return (
<SpinnerButton
className={classNames(className, !isLastButton && styles.middleButton)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
return (
<Button
className={classNames(className, !isLastButton && styles.middleButton)}
kind={kinds.PRIMARY}
{...otherProps}
/>
);
}
export default FormInputButton;
@@ -272,6 +272,8 @@ FormInputGroup.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.any, value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any), values: PropTypes.arrayOf(PropTypes.any),
placeholder: PropTypes.string,
delimiters: PropTypes.arrayOf(PropTypes.string),
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
type: PropTypes.string.isRequired, type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all), kind: PropTypes.oneOf(kinds.all),
@@ -284,8 +286,10 @@ FormInputGroup.propTypes = {
helpTextWarning: PropTypes.string, helpTextWarning: PropTypes.string,
helpLink: PropTypes.string, helpLink: PropTypes.string,
autoFocus: PropTypes.bool, autoFocus: PropTypes.bool,
canEdit: PropTypes.bool,
includeNoChange: PropTypes.bool, includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool,
includeAny: PropTypes.bool,
selectedValueOptions: PropTypes.object, selectedValueOptions: PropTypes.object,
indexerFlags: PropTypes.number, indexerFlags: PropTypes.number,
pending: PropTypes.bool, pending: PropTypes.bool,
@@ -5,17 +5,20 @@ import translate from 'Utilities/String/translate';
import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput';
function MovieMonitoredSelectInput(props) { function MovieMonitoredSelectInput(props) {
const values = [...monitorOptions];
const { const {
includeNoChange, includeNoChange,
includeMixed includeMixed,
...otherProps
} = props; } = props;
const values = [...monitorOptions];
if (includeNoChange) { if (includeNoChange) {
values.unshift({ values.unshift({
key: 'noChange', key: 'noChange',
value: translate('NoChange'), get value() {
return translate('NoChange');
},
isDisabled: true isDisabled: true
}); });
} }
@@ -23,14 +26,16 @@ function MovieMonitoredSelectInput(props) {
if (includeMixed) { if (includeMixed) {
values.unshift({ values.unshift({
key: 'mixed', key: 'mixed',
value: '(Mixed)', get value() {
return `(${translate('Mixed')})`;
},
isDisabled: true isDisabled: true
}); });
} }
return ( return (
<EnhancedSelectInput <EnhancedSelectInput
{...props} {...otherProps}
values={values} values={values}
/> />
); );
+4
View File
@@ -1,3 +1,7 @@
.wrapper {
display: inline-block;
}
.image { .image {
align-content: center; align-content: center;
margin-right: 5px; margin-right: 5px;
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'image': string; 'image': string;
'wrapper': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
+1 -1
View File
@@ -23,7 +23,7 @@ function ImdbRating(props: ImdbRatingProps) {
return ( return (
<Tooltip <Tooltip
anchor={ anchor={
<span> <span className={styles.wrapper}>
{!hideIcon && ( {!hideIcon && (
<img <img
className={styles.image} className={styles.image}
@@ -1,139 +0,0 @@
import Clipboard from 'clipboard';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormInputButton from 'Components/Form/FormInputButton';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import styles from './ClipboardButton.css';
class ClipboardButton extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._id = getUniqueElememtId();
this._successTimeout = null;
this._testResultTimeout = null;
this.state = {
showSuccess: false,
showError: false
};
}
componentDidMount() {
this._clipboard = new Clipboard(`#${this._id}`, {
text: () => this.props.value,
container: document.getElementById(this._id)
});
this._clipboard.on('success', this.onSuccess);
}
componentDidUpdate() {
const {
showSuccess,
showError
} = this.state;
if (showSuccess || showError) {
this._testResultTimeout = setTimeout(this.resetState, 3000);
}
}
componentWillUnmount() {
if (this._clipboard) {
this._clipboard.destroy();
}
if (this._testResultTimeout) {
clearTimeout(this._testResultTimeout);
}
}
//
// Control
resetState = () => {
this.setState({
showSuccess: false,
showError: false
});
};
//
// Listeners
onSuccess = () => {
this.setState({
showSuccess: true
});
};
onError = () => {
this.setState({
showError: true
});
};
//
// Render
render() {
const {
value,
className,
...otherProps
} = this.props;
const {
showSuccess,
showError
} = this.state;
const showStateIcon = showSuccess || showError;
const iconName = showError ? icons.DANGER : icons.CHECK;
const iconKind = showError ? kinds.DANGER : kinds.SUCCESS;
return (
<FormInputButton
id={this._id}
className={className}
{...otherProps}
>
<span className={showStateIcon ? styles.showStateIcon : undefined}>
{
showSuccess &&
<span className={styles.stateIconContainer}>
<Icon
name={iconName}
kind={iconKind}
/>
</span>
}
{
<span className={styles.clipboardIconContainer}>
<Icon name={icons.CLIPBOARD} />
</span>
}
</span>
</FormInputButton>
);
}
}
ClipboardButton.propTypes = {
className: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
};
ClipboardButton.defaultProps = {
className: styles.button
};
export default ClipboardButton;
@@ -0,0 +1,76 @@
import copy from 'copy-to-clipboard';
import React, { useCallback, useEffect, useState } from 'react';
import FormInputButton from 'Components/Form/FormInputButton';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import { ButtonProps } from './Button';
import styles from './ClipboardButton.css';
export interface ClipboardButtonProps extends Omit<ButtonProps, 'children'> {
value: string;
}
export type ClipboardState = 'success' | 'error' | null;
export default function ClipboardButton({
id,
value,
className = styles.button,
...otherProps
}: ClipboardButtonProps) {
const [state, setState] = useState<ClipboardState>(null);
useEffect(() => {
if (!state) {
return;
}
const timeoutId = setTimeout(() => {
setState(null);
}, 3000);
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [state]);
const handleClick = useCallback(async () => {
try {
if ('clipboard' in navigator) {
await navigator.clipboard.writeText(value);
} else {
copy(value);
}
setState('success');
} catch (e) {
setState('error');
console.error(`Failed to copy to clipboard`, e);
}
}, [value]);
return (
<FormInputButton
className={className}
onClick={handleClick}
{...otherProps}
>
<span className={state ? styles.showStateIcon : undefined}>
{state ? (
<span className={styles.stateIconContainer}>
<Icon
name={state === 'error' ? icons.DANGER : icons.CHECK}
kind={state === 'error' ? kinds.DANGER : kinds.SUCCESS}
/>
</span>
) : null}
<span className={styles.clipboardIconContainer}>
<Icon name={icons.CLIPBOARD} />
</span>
</span>
</FormInputButton>
);
}
@@ -34,7 +34,7 @@ function getSuggestions(movies, value) {
key: 'title' key: 'title'
} }
], ],
arrayIndex: 0 refIndex: 0
}); });
if (suggestions.length > limit) { if (suggestions.length > limit) {
break; break;
@@ -1,3 +1,7 @@
.wrapper {
display: inline-block;
}
.image { .image {
align-content: center; align-content: center;
margin-right: 5px; margin-right: 5px;
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'image': string; 'image': string;
'wrapper': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -24,7 +24,7 @@ function RottenTomatoRating(props: RottenTomatoRatingProps) {
const ratingImage = value > 50 ? rtFresh : rtRotten; const ratingImage = value > 50 ? rtFresh : rtRotten;
return ( return (
<span> <span className={styles.wrapper}>
{!hideIcon && ( {!hideIcon && (
<img <img
className={styles.image} className={styles.image}
+41 -1
View File
@@ -168,7 +168,7 @@ class SignalRConnector extends Component {
const status = resource.status; const status = resource.status;
// Both successful and failed commands need to be // Both successful and failed commands need to be
// completed, otherwise they spin until they timeout. // completed, otherwise they spin until they time out.
if (status === 'completed' || status === 'failed') { if (status === 'completed' || status === 'failed') {
this.props.dispatchFinishCommand(resource); this.props.dispatchFinishCommand(resource);
@@ -192,10 +192,50 @@ class SignalRConnector extends Component {
} }
}; };
handleDownloadclient = ({ action, resource }) => {
const section = 'settings.downloadClients';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleHealth = () => { handleHealth = () => {
this.props.dispatchFetchHealth(); this.props.dispatchFetchHealth();
}; };
handleImportlist = ({ action, resource }) => {
const section = 'settings.importLists';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleIndexer = ({ action, resource }) => {
const section = 'settings.indexers';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleNotification = ({ action, resource }) => {
const section = 'settings.notifications';
if (action === 'created' || action === 'updated') {
this.props.dispatchUpdateItem({ section, ...resource });
} else if (action === 'deleted') {
this.props.dispatchRemoveItem({ section, id: resource.id });
}
};
handleMovie = (body) => { handleMovie = (body) => {
const action = body.action; const action = body.action;
const section = 'movies'; const section = 'movies';
+4
View File
@@ -1,3 +1,7 @@
.wrapper {
display: inline-block;
}
.image { .image {
align-content: center; align-content: center;
margin-right: 5px; margin-right: 5px;
+1
View File
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'image': string; 'image': string;
'wrapper': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
+1 -1
View File
@@ -22,7 +22,7 @@ function TmdbRating(props: TmdbRatingProps) {
return ( return (
<Tooltip <Tooltip
anchor={ anchor={
<span> <span className={styles.wrapper}>
{!hideIcon && ( {!hideIcon && (
<img <img
className={styles.image} className={styles.image}
+9
View File
@@ -0,0 +1,9 @@
.wrapper {
display: inline-block;
}
.image {
align-content: center;
margin-right: 5px;
vertical-align: -0.125em;
}
@@ -1,7 +1,8 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'blankpad': string; 'image': string;
'wrapper': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
+46
View File
@@ -0,0 +1,46 @@
import React from 'react';
import Tooltip from 'Components/Tooltip/Tooltip';
import { kinds, tooltipPositions } from 'Helpers/Props';
import { Ratings } from 'Movie/Movie';
import translate from 'Utilities/String/translate';
import styles from './TraktRating.css';
interface TraktRatingProps {
ratings: Ratings;
iconSize?: number;
hideIcon?: boolean;
}
function TraktRating(props: TraktRatingProps) {
const { ratings, iconSize = 14, hideIcon = false } = props;
const traktImage =
'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiAgICAgdmlld0JveD0iMCAwIDE0NC44IDE0NC44IiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAxNDQuOCAxNDQuOCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PGc+ICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik0yOS41LDExMS44YzEwLjYsMTEuNiwyNS45LDE4LjgsNDIuOSwxOC44YzguNywwLDE2LjktMS45LDI0LjMtNS4zTDU2LjMsODVMMjkuNSwxMTEuOHoiLz4gICAgPHBhdGggZmlsbD0iI0VEMjIyNCIgZD0iTTU2LjEsNjAuNkwyNS41LDkxLjFMMjEuNCw4N2wzMi4yLTMyLjJoMGwzNy42LTM3LjZjLTUuOS0yLTEyLjItMy4xLTE4LjgtMy4xYy0zMi4yLDAtNTguMywyNi4xLTU4LjMsNTguMyAgICAgICBjMCwxMy4xLDQuMywyNS4yLDExLjcsMzVsMzAuNS0zMC41bDIuMSwybDQzLjcsNDMuN2MwLjktMC41LDEuNy0xLDIuNS0xLjZMNTYuMyw3Mi43TDI3LDEwMmwtNC4xLTQuMWwzMy40LTMzLjRsMi4xLDJsNTEsNTAuOSAgICAgICBjMC44LTAuNiwxLjUtMS4zLDIuMi0xLjlsLTU1LTU1TDU2LjEsNjAuNnoiLz4gICAgPHBhdGggZmlsbD0iI0VEMUMyNCIgZD0iTTExNS43LDExMS40YzkuMy0xMC4zLDE1LTI0LDE1LTM5YzAtMjMuNC0xMy44LTQzLjUtMzMuNi01Mi44TDYwLjQsNTYuMkwxMTUuNywxMTEuNHogTTc0LjUsNjYuOGwtNC4xLTQuMSAgICAgICBsMjguOS0yOC45bDQuMSw0LjFMNzQuNSw2Ni44eiBNMTAxLjksMjcuMUw2OC42LDYwLjRsLTQuMS00LjFMOTcuOCwyM0wxMDEuOSwyNy4xeiIvPiAgICA8Zz4gICAgICAgPGc+ICAgICAgICAgIDxwYXRoIGZpbGw9IiNFRDIyMjQiIGQ9Ik03Mi40LDE0NC44QzMyLjUsMTQ0LjgsMCwxMTIuMywwLDcyLjRDMCwzMi41LDMyLjUsMCw3Mi40LDBzNzIuNCwzMi41LDcyLjQsNzIuNCAgICAgICAgICAgICBDMTQ0LjgsMTEyLjMsMTEyLjMsMTQ0LjgsNzIuNCwxNDQuOHogTTcyLjQsNy4zQzM2LjUsNy4zLDcuMywzNi41LDcuMyw3Mi40czI5LjIsNjUuMSw2NS4xLDY1LjFzNjUuMS0yOS4yLDY1LjEtNjUuMSAgICAgICAgICAgICBTMTA4LjMsNy4zLDcyLjQsNy4zeiIvPiAgICAgICA8L2c+ICAgIDwvZz48L2c+PC9zdmc+';
const { value = 0, votes = 0 } = ratings.trakt;
return (
<Tooltip
anchor={
<span className={styles.wrapper}>
{!hideIcon && (
<img
className={styles.image}
alt={translate('TraktRating')}
src={traktImage}
style={{
height: `${iconSize}px`,
}}
/>
)}
{(value * 10).toFixed()}%
</span>
}
tooltip={translate('CountVotes', { votes })}
kind={kinds.INVERSE}
position={tooltipPositions.TOP}
/>
);
}
export default TraktRating;
@@ -110,6 +110,15 @@ function DiscoverMovieSortMenu(props) {
{translate('RottenTomatoesRating')} {translate('RottenTomatoesRating')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem
name="traktRating"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('TraktRating')}
</SortMenuItem>
<SortMenuItem <SortMenuItem
name="certification" name="certification"
sortKey={sortKey} sortKey={sortKey}
@@ -9,6 +9,7 @@ import Link from 'Components/Link/Link';
import RottenTomatoRating from 'Components/RottenTomatoRating'; import RottenTomatoRating from 'Components/RottenTomatoRating';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import TraktRating from 'Components/TraktRating';
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal'; import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal'; import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
@@ -92,6 +93,7 @@ class DiscoverMoviePoster extends Component {
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating, showRottenTomatoesRating,
showTraktRating,
ratings, ratings,
isExisting, isExisting,
isExcluded, isExcluded,
@@ -223,6 +225,12 @@ class DiscoverMoviePoster extends Component {
</div> </div>
) : null} ) : null}
{showTraktRating && !!ratings.trakt ? (
<div className={styles.title}>
<TraktRating ratings={ratings} iconSize={12} />
</div>
) : null}
<DiscoverMoviePosterInfo <DiscoverMoviePosterInfo
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
@@ -232,6 +240,7 @@ class DiscoverMoviePoster extends Component {
showTmdbRating={showTmdbRating} showTmdbRating={showTmdbRating}
showImdbRating={showImdbRating} showImdbRating={showImdbRating}
showRottenTomatoesRating={showRottenTomatoesRating} showRottenTomatoesRating={showRottenTomatoesRating}
showTraktRating={showTraktRating}
{...otherProps} {...otherProps}
/> />
@@ -274,6 +283,7 @@ DiscoverMoviePoster.propTypes = {
showTmdbRating: PropTypes.bool.isRequired, showTmdbRating: PropTypes.bool.isRequired,
showImdbRating: PropTypes.bool.isRequired, showImdbRating: PropTypes.bool.isRequired,
showRottenTomatoesRating: PropTypes.bool.isRequired, showRottenTomatoesRating: PropTypes.bool.isRequired,
showTraktRating: PropTypes.bool.isRequired,
ratings: PropTypes.object.isRequired, ratings: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
@@ -4,6 +4,7 @@ import Icon from 'Components/Icon';
import ImdbRating from 'Components/ImdbRating'; import ImdbRating from 'Components/ImdbRating';
import RottenTomatoRating from 'Components/RottenTomatoRating'; import RottenTomatoRating from 'Components/RottenTomatoRating';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import TraktRating from 'Components/TraktRating';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import getMovieStatusDetails from 'Movie/getMovieStatusDetails'; import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
import formatRuntime from 'Utilities/Date/formatRuntime'; import formatRuntime from 'Utilities/Date/formatRuntime';
@@ -28,7 +29,8 @@ function DiscoverMoviePosterInfo(props) {
movieRuntimeFormat, movieRuntimeFormat,
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating showRottenTomatoesRating,
showTraktRating
} = props; } = props;
if (sortKey === 'status' && status) { if (sortKey === 'status' && status) {
@@ -141,6 +143,14 @@ function DiscoverMoviePosterInfo(props) {
); );
} }
if (!showTraktRating && sortKey === 'traktRating' && !!ratings.trakt) {
return (
<div className={styles.info}>
<TraktRating ratings={ratings} iconSize={12} />
</div>
);
}
return null; return null;
} }
@@ -160,7 +170,8 @@ DiscoverMoviePosterInfo.propTypes = {
movieRuntimeFormat: PropTypes.string.isRequired, movieRuntimeFormat: PropTypes.string.isRequired,
showTmdbRating: PropTypes.bool.isRequired, showTmdbRating: PropTypes.bool.isRequired,
showImdbRating: PropTypes.bool.isRequired, showImdbRating: PropTypes.bool.isRequired,
showRottenTomatoesRating: PropTypes.bool.isRequired showRottenTomatoesRating: PropTypes.bool.isRequired,
showTraktRating: PropTypes.bool.isRequired
}; };
export default DiscoverMoviePosterInfo; export default DiscoverMoviePosterInfo;
@@ -39,7 +39,8 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
showTitle, showTitle,
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating showRottenTomatoesRating,
showTraktRating
} = posterOptions; } = posterOptions;
const heights = [ const heights = [
@@ -64,6 +65,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
heights.push(19); heights.push(19);
} }
if (showTraktRating) {
heights.push(19);
}
switch (sortKey) { switch (sortKey) {
case 'studio': case 'studio':
case 'inCinemas': case 'inCinemas':
@@ -88,6 +93,11 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
heights.push(19); heights.push(19);
} }
break; break;
case 'traktRating':
if (!showTraktRating) {
heights.push(19);
}
break;
default: default:
// No need to add a height of 0 // No need to add a height of 0
} }
@@ -219,7 +229,8 @@ class DiscoverMoviePosters extends Component {
showTitle, showTitle,
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating showRottenTomatoesRating,
showTraktRating
} = posterOptions; } = posterOptions;
const movieIdx = rowIndex * columnCount + columnIndex; const movieIdx = rowIndex * columnCount + columnIndex;
@@ -248,6 +259,7 @@ class DiscoverMoviePosters extends Component {
showTmdbRating={showTmdbRating} showTmdbRating={showTmdbRating}
showImdbRating={showImdbRating} showImdbRating={showImdbRating}
showRottenTomatoesRating={showRottenTomatoesRating} showRottenTomatoesRating={showRottenTomatoesRating}
showTraktRating={showTraktRating}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
@@ -48,6 +48,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
showTmdbRating: props.showTmdbRating, showTmdbRating: props.showTmdbRating,
showImdbRating: props.showImdbRating, showImdbRating: props.showImdbRating,
showRottenTomatoesRating: props.showRottenTomatoesRating, showRottenTomatoesRating: props.showRottenTomatoesRating,
showTraktRating: props.showTraktRating,
includeRecommendations: props.includeRecommendations, includeRecommendations: props.includeRecommendations,
includeTrending: props.includeTrending, includeTrending: props.includeTrending,
includePopular: props.includePopular includePopular: props.includePopular
@@ -61,6 +62,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating, showRottenTomatoesRating,
showTraktRating,
includeRecommendations, includeRecommendations,
includeTrending, includeTrending,
includePopular includePopular
@@ -88,6 +90,10 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
state.showRottenTomatoesRating = showRottenTomatoesRating; state.showRottenTomatoesRating = showRottenTomatoesRating;
} }
if (showTraktRating !== prevProps.showTraktRating) {
state.showTraktRating = showTraktRating;
}
if (includeRecommendations !== prevProps.includeRecommendations) { if (includeRecommendations !== prevProps.includeRecommendations) {
state.includeRecommendations = includeRecommendations; state.includeRecommendations = includeRecommendations;
} }
@@ -140,6 +146,7 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating, showRottenTomatoesRating,
showTraktRating,
includeRecommendations, includeRecommendations,
includeTrending, includeTrending,
includePopular includePopular
@@ -248,6 +255,18 @@ class DiscoverMoviePosterOptionsModalContent extends Component {
onChange={this.onChangePosterOption} onChange={this.onChangePosterOption}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showTraktRating"
value={showTraktRating}
helpText={translate('ShowTraktRatingPosterHelpText')}
onChange={this.onChangePosterOption}
/>
</FormGroup>
</Form> </Form>
</ModalBody> </ModalBody>
@@ -269,6 +288,7 @@ DiscoverMoviePosterOptionsModalContent.propTypes = {
showTmdbRating: PropTypes.bool.isRequired, showTmdbRating: PropTypes.bool.isRequired,
showImdbRating: PropTypes.bool.isRequired, showImdbRating: PropTypes.bool.isRequired,
showRottenTomatoesRating: PropTypes.bool.isRequired, showRottenTomatoesRating: PropTypes.bool.isRequired,
showTraktRating: PropTypes.bool.isRequired,
includeRecommendations: PropTypes.bool.isRequired, includeRecommendations: PropTypes.bool.isRequired,
includeTrending: PropTypes.bool.isRequired, includeTrending: PropTypes.bool.isRequired,
includePopular: PropTypes.bool.isRequired, includePopular: PropTypes.bool.isRequired,
@@ -35,6 +35,7 @@
.tmdbRating, .tmdbRating,
.imdbRating, .imdbRating,
.rottenTomatoesRating, .rottenTomatoesRating,
.traktRating,
.runtime { .runtime {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
@@ -21,6 +21,7 @@ interface CssExports {
'status': string; 'status': string;
'studio': string; 'studio': string;
'tmdbRating': string; 'tmdbRating': string;
'traktRating': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -60,6 +60,7 @@
.tmdbRating, .tmdbRating,
.imdbRating, .imdbRating,
.rottenTomatoesRating, .rottenTomatoesRating,
.traktRating,
.runtime { .runtime {
composes: cell; composes: cell;
@@ -27,6 +27,7 @@ interface CssExports {
'statusIcon': string; 'statusIcon': string;
'studio': string; 'studio': string;
'tmdbRating': string; 'tmdbRating': string;
'traktRating': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -11,6 +11,7 @@ import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import TraktRating from 'Components/TraktRating';
import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal'; import AddNewDiscoverMovieModal from 'DiscoverMovie/AddNewDiscoverMovieModal';
import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal'; import ExcludeMovieModal from 'DiscoverMovie/Exclusion/ExcludeMovieModal';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
@@ -291,6 +292,17 @@ class DiscoverMovieRow extends Component {
); );
} }
if (name === 'traktRating') {
return (
<VirtualTableRowCell
key={name}
className={styles[name]}
>
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
</VirtualTableRowCell>
);
}
if (name === 'popularity') { if (name === 'popularity') {
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
@@ -1,175 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCastPoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isEditImportListModalOpen: false
};
}
//
// Listeners
onEditImportListPress = () => {
this.setState({ isEditImportListModalOpen: true });
};
onAddImportListPress = () => {
this.props.onImportListSelect();
this.setState({ isEditImportListModalOpen: true });
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
};
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
};
//
// Render
render() {
const {
tmdbId,
personName,
character,
images,
posterWidth,
posterHeight,
importList
} = this.props;
const {
hasPosterError
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div
style={elementStyle}
>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{personName}
</div>
}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{character}
</div>
<EditImportListModalConnector
id={importListId}
isOpen={this.state.isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
onDeleteImportListPress={this.onDeleteImportListPress}
/>
</div>
);
}
}
MovieCastPoster.propTypes = {
tmdbId: PropTypes.number.isRequired,
personName: PropTypes.string.isRequired,
character: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
export default MovieCastPoster;
@@ -0,0 +1,179 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
export interface MovieCastPosterProps
extends Pick<MovieCredit, 'personName' | 'images' | 'character'> {
tmdbId: number;
posterWidth: number;
posterHeight: number;
importList?: ImportList;
onImportListSelect(): void;
}
function MovieCastPoster(props: MovieCastPosterProps) {
const {
tmdbId,
personName,
character,
images = [],
posterWidth,
posterHeight,
importList,
onImportListSelect,
} = props;
const importListId = importList?.id ?? 0;
const dispatch = useDispatch();
const [hasPosterError, setHasPosterError] = useState(false);
const [
isEditImportListModalOpen,
setEditImportListModalOpen,
setEditImportListModalClosed,
] = useModalOpenState(false);
const [
isDeleteImportListModalOpen,
setDeleteImportListModalOpen,
setDeleteImportListModalClosed,
] = useModalOpenState(false);
const handlePosterLoadError = useCallback(() => {
setHasPosterError(true);
}, [setHasPosterError]);
const handlePosterLoad = useCallback(() => {
setHasPosterError(false);
}, [setHasPosterError]);
const handleManageImportListPress = useCallback(() => {
if (importListId === 0) {
onImportListSelect();
}
setEditImportListModalOpen();
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
const handleDeleteImportListConfirmed = useCallback(() => {
dispatch(deleteImportList({ id: importListId }));
setEditImportListModalClosed();
setDeleteImportListModalClosed();
}, [
importListId,
setEditImportListModalClosed,
setDeleteImportListModalClosed,
dispatch,
]);
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px',
};
const contentStyle = {
width: `${posterWidth}px`,
};
const monitored =
importList?.enabled === true && importList?.enableAuto === true;
return (
<div className={styles.content} style={contentStyle}>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={handleManageImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div style={elementStyle}>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={handlePosterLoadError}
onLoad={handlePosterLoad}
/>
{hasPosterError && (
<div className={styles.overlayTitle}>{personName}</div>
)}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{character}
</div>
<EditImportListModalConnector
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}
onDeleteImportListPress={setDeleteImportListModalOpen}
/>
<ConfirmModal
isOpen={isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', {
name: importList?.name ?? personName,
})}
confirmLabel={translate('Delete')}
onConfirm={handleDeleteImportListConfirmed}
onCancel={setDeleteImportListModalClosed}
/>
</div>
);
}
export default MovieCastPoster;
@@ -0,0 +1,25 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCastPoster from './MovieCastPoster';
interface MovieCastPostersProps {
isSmallScreen: boolean;
}
function MovieCastPosters({ isSmallScreen }: MovieCastPostersProps) {
const { items: castCredits } = useSelector(
createMovieCreditsSelector('cast')
);
return (
<MovieCreditPosters
items={castCredits}
itemComponent={MovieCastPoster}
isSmallScreen={isSmallScreen}
/>
);
}
export default MovieCastPosters;
@@ -1,43 +0,0 @@
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCastPoster from './MovieCastPoster';
function createMapStateToProps() {
return createSelector(
(state) => state.movieCredits.items,
(credits) => {
const cast = _.reduce(credits, (acc, credit) => {
if (credit.type === 'cast') {
acc.push(credit);
}
return acc;
}, []);
return {
items: cast
};
}
);
}
class MovieCastPostersConnector extends Component {
//
// Render
render() {
return (
<MovieCreditPosters
{...this.props}
itemComponent={MovieCastPoster}
/>
);
}
}
export default connect(createMapStateToProps)(MovieCastPostersConnector);
@@ -1,175 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
class MovieCrewPoster extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isEditImportListModalOpen: false
};
}
//
// Listeners
onEditImportListPress = () => {
this.setState({ isEditImportListModalOpen: true });
};
onAddImportListPress = () => {
this.props.onImportListSelect();
this.setState({ isEditImportListModalOpen: true });
};
onEditImportListModalClose = () => {
this.setState({ isEditImportListModalOpen: false });
};
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
};
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
};
//
// Render
render() {
const {
tmdbId,
personName,
job,
images,
posterWidth,
posterHeight,
importList
} = this.props;
const {
hasPosterError
} = this.state;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px'
};
const contentStyle = {
width: `${posterWidth}px`
};
const monitored = importList !== undefined && importList.enabled && importList.enableAuto;
const importListId = importList ? importList.id : 0;
return (
<div
className={styles.content}
style={contentStyle}
>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div
style={elementStyle}
>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
{
hasPosterError &&
<div className={styles.overlayTitle}>
{personName}
</div>
}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{job}
</div>
<EditImportListModalConnector
id={importListId}
isOpen={this.state.isEditImportListModalOpen}
onModalClose={this.onEditImportListModalClose}
onDeleteImportListPress={this.onDeleteImportListPress}
/>
</div>
);
}
}
MovieCrewPoster.propTypes = {
tmdbId: PropTypes.number.isRequired,
personName: PropTypes.string.isRequired,
job: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
importList: PropTypes.object,
onImportListSelect: PropTypes.func.isRequired
};
export default MovieCrewPoster;
@@ -0,0 +1,177 @@
import classNames from 'classnames';
import React, { useCallback, useState } from 'react';
import { useDispatch } from 'react-redux';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { icons, kinds, sizes } from 'Helpers/Props';
import MovieHeadshot from 'Movie/MovieHeadshot';
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
import { deleteImportList } from 'Store/Actions/Settings/importLists';
import ImportList from 'typings/ImportList';
import MovieCredit from 'typings/MovieCredit';
import translate from 'Utilities/String/translate';
import styles from '../MovieCreditPoster.css';
export interface MovieCrewPosterProps
extends Pick<MovieCredit, 'personName' | 'images' | 'job'> {
tmdbId: number;
posterWidth: number;
posterHeight: number;
importList?: ImportList;
onImportListSelect(): void;
}
function MovieCrewPoster(props: MovieCrewPosterProps) {
const {
tmdbId,
personName,
job,
images = [],
posterWidth,
posterHeight,
importList,
onImportListSelect,
} = props;
const importListId = importList?.id ?? 0;
const dispatch = useDispatch();
const [hasPosterError, setHasPosterError] = useState(false);
const [
isEditImportListModalOpen,
setEditImportListModalOpen,
setEditImportListModalClosed,
] = useModalOpenState(false);
const [
isDeleteImportListModalOpen,
setDeleteImportListModalOpen,
setDeleteImportListModalClosed,
] = useModalOpenState(false);
const handlePosterLoadError = useCallback(() => {
setHasPosterError(true);
}, [setHasPosterError]);
const handlePosterLoad = useCallback(() => {
setHasPosterError(false);
}, [setHasPosterError]);
const handleManageImportListPress = useCallback(() => {
if (importListId === 0) {
onImportListSelect();
}
setEditImportListModalOpen();
}, [importListId, onImportListSelect, setEditImportListModalOpen]);
const handleDeleteImportListConfirmed = useCallback(() => {
dispatch(deleteImportList({ id: importListId }));
setEditImportListModalClosed();
setDeleteImportListModalClosed();
}, [
importListId,
setEditImportListModalClosed,
setDeleteImportListModalClosed,
dispatch,
]);
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px',
};
const contentStyle = {
width: `${posterWidth}px`,
};
const monitored =
importList?.enabled === true && importList?.enableAuto === true;
return (
<div className={styles.content} style={contentStyle}>
<div className={styles.posterContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={handleManageImportListPress}
/>
</div>
<Label className={styles.controls}>
<span className={styles.externalLinks}>
<Popover
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
title={translate('Links')}
body={
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
<Label
className={styles.externalLinkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
{translate('TMDb')}
</Label>
</Link>
}
/>
</span>
</Label>
<div style={elementStyle}>
<MovieHeadshot
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={handlePosterLoadError}
onLoad={handlePosterLoad}
/>
{hasPosterError && (
<div className={styles.overlayTitle}>{personName}</div>
)}
</div>
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>
{personName}
</div>
<div className={classNames(styles.title, 'swiper-no-swiping')}>{job}</div>
<EditImportListModalConnector
id={importListId}
isOpen={isEditImportListModalOpen}
onModalClose={setEditImportListModalClosed}
onDeleteImportListPress={setDeleteImportListModalOpen}
/>
<ConfirmModal
isOpen={isDeleteImportListModalOpen}
kind={kinds.DANGER}
title={translate('DeleteImportList')}
message={translate('DeleteImportListMessageText', {
name: importList?.name ?? personName,
})}
confirmLabel={translate('Delete')}
onConfirm={handleDeleteImportListConfirmed}
onCancel={setDeleteImportListModalClosed}
/>
</div>
);
}
export default MovieCrewPoster;
@@ -0,0 +1,25 @@
import React from 'react';
import { useSelector } from 'react-redux';
import createMovieCreditsSelector from 'Store/Selectors/createMovieCreditsSelector';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster';
interface MovieCrewPostersProps {
isSmallScreen: boolean;
}
function MovieCrewPosters({ isSmallScreen }: MovieCrewPostersProps) {
const { items: crewCredits } = useSelector(
createMovieCreditsSelector('crew')
);
return (
<MovieCreditPosters
items={crewCredits}
itemComponent={MovieCrewPoster}
isSmallScreen={isSmallScreen}
/>
);
}
export default MovieCrewPosters;
@@ -1,68 +0,0 @@
import _ from 'lodash';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieCreditPosters from '../MovieCreditPosters';
import MovieCrewPoster from './MovieCrewPoster';
function crewSort(a, b) {
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
const indexA = jobOrder.indexOf(a.job);
const indexB = jobOrder.indexOf(b.job);
if (indexA === -1 && indexB === -1) {
return 0;
} else if (indexA === -1) {
return 1;
} else if (indexB === -1) {
return -1;
}
if (indexA < indexB) {
return -1;
} else if (indexA > indexB) {
return 1;
}
return 0;
}
function createMapStateToProps() {
return createSelector(
(state) => state.movieCredits.items,
(credits) => {
const crew = _.reduce(credits, (acc, credit) => {
if (credit.type === 'crew') {
acc.push(credit);
}
return acc;
}, []);
const sortedCrew = crew.sort(crewSort);
return {
items: _.uniqBy(sortedCrew, 'personName')
};
}
);
}
class MovieCrewPostersConnector extends Component {
//
// Render
render() {
return (
<MovieCreditPosters
{...this.props}
itemComponent={MovieCrewPoster}
/>
);
}
}
export default connect(createMapStateToProps)(MovieCrewPostersConnector);
@@ -0,0 +1,60 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
selectImportListSchema,
setImportListFieldValue,
setImportListValue,
} from 'Store/Actions/settingsActions';
import createMovieCreditImportListSelector from 'Store/Selectors/createMovieCreditImportListSelector';
import { MovieCastPosterProps } from './Cast/MovieCastPoster';
import { MovieCrewPosterProps } from './Crew/MovieCrewPoster';
type MovieCreditPosterProps = {
component: React.ElementType;
} & (
| Omit<MovieCrewPosterProps, 'onImportListSelect'>
| Omit<MovieCastPosterProps, 'onImportListSelect'>
);
function MovieCreditPoster({
component: ItemComponent,
tmdbId,
personName,
...otherProps
}: MovieCreditPosterProps) {
const importList = useSelector(createMovieCreditImportListSelector(tmdbId));
const dispatch = useDispatch();
const handleImportListSelect = useCallback(() => {
dispatch(
selectImportListSchema({
implementation: 'TMDbPersonImport',
implementationName: 'TMDb Person',
presetName: undefined,
})
);
dispatch(
// @ts-expect-error 'setImportListFieldValue' isn't typed yet
setImportListFieldValue({ name: 'personId', value: tmdbId.toString() })
);
dispatch(
// @ts-expect-error 'setImportListValue' isn't typed yet
setImportListValue({ name: 'name', value: `${personName} - ${tmdbId}` })
);
}, [dispatch, tmdbId, personName]);
return (
<ItemComponent
{...otherProps}
tmdbId={tmdbId}
personName={personName}
importList={importList}
onImportListSelect={handleImportListSelect}
/>
);
}
export default MovieCreditPoster;
@@ -1,66 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { selectImportListSchema, setImportListFieldValue, setImportListValue } from 'Store/Actions/settingsActions';
import createMovieCreditListSelector from 'Store/Selectors/createMovieCreditListSelector';
function createMapStateToProps() {
return createSelector(
createMovieCreditListSelector(),
(importList) => {
return {
importList
};
}
);
}
const mapDispatchToProps = {
selectImportListSchema,
setImportListFieldValue,
setImportListValue
};
class MovieCreditPosterConnector extends Component {
//
// Listeners
onImportListSelect = () => {
this.props.selectImportListSchema({ implementation: 'TMDbPersonImport', implementationName: 'TMDb Person', presetName: undefined });
this.props.setImportListFieldValue({ name: 'personId', value: this.props.tmdbId.toString() });
this.props.setImportListValue({ name: 'name', value: `${this.props.personName} - ${this.props.tmdbId}` });
};
//
// Render
render() {
const {
tmdbId,
component: ItemComponent,
personName
} = this.props;
return (
<ItemComponent
{...this.props}
tmdbId={tmdbId}
personName={personName}
onImportListSelect={this.onImportListSelect}
/>
);
}
}
MovieCreditPosterConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
personName: PropTypes.string.isRequired,
component: PropTypes.elementType.isRequired,
selectImportListSchema: PropTypes.func.isRequired,
setImportListFieldValue: PropTypes.func.isRequired,
setImportListValue: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(MovieCreditPosterConnector);
@@ -1,109 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import dimensions from 'Styles/Variables/dimensions';
import MovieCreditPosterConnector from './MovieCreditPosterConnector';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
function calculateRowHeight(posterHeight, isSmallScreen) {
const titleHeight = 19;
const characterHeight = 19;
const heights = [
posterHeight,
titleHeight,
characterHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding
];
return heights.reduce((acc, height) => acc + height, 0);
}
class MovieCreditPosters extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnWidth: 182,
columnCount: 1,
posterWidth: 162,
posterHeight: 238,
rowHeight: calculateRowHeight(238, props.isSmallScreen)
};
}
//
// Render
render() {
const {
items,
itemComponent,
isSmallScreen
} = this.props;
const {
posterWidth,
posterHeight,
rowHeight
} = this.state;
return (
<div className={styles.sliderContainer}>
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={isSmallScreen ? 1 : 3}
navigation={true}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.navigation.init();
swiper.navigation.update();
}}
>
{items.map((credit) => (
<SwiperSlide key={credit.id} style={{ width: posterWidth, height: rowHeight }}>
<MovieCreditPosterConnector
key={credit.id}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
job={credit.job}
character={credit.character}
images={credit.images}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}
}
MovieCreditPosters.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
itemComponent: PropTypes.elementType.isRequired,
isSmallScreen: PropTypes.bool.isRequired
};
export default MovieCreditPosters;
@@ -0,0 +1,87 @@
import React, { useCallback, useMemo } from 'react';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
import { Swiper as SwiperClass } from 'swiper/types';
import dimensions from 'Styles/Variables/dimensions';
import MovieCredit from 'typings/MovieCredit';
import MovieCreditPoster from './MovieCreditPoster';
import styles from './MovieCreditPosters.css';
// Import Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(
dimensions.movieIndexColumnPaddingSmallScreen
);
interface MovieCreditPostersProps {
items: MovieCredit[];
itemComponent: React.ElementType;
isSmallScreen: boolean;
}
function MovieCreditPosters(props: MovieCreditPostersProps) {
const { items, itemComponent, isSmallScreen } = props;
const posterWidth = 162;
const posterHeight = 238;
const rowHeight = useMemo(() => {
const titleHeight = 19;
const characterHeight = 19;
const heights = [
posterHeight,
titleHeight,
characterHeight,
isSmallScreen ? columnPaddingSmallScreen : columnPadding,
];
return heights.reduce((acc, height) => acc + height, 0);
}, [posterHeight, isSmallScreen]);
const handleSwiperInit = useCallback((swiper: SwiperClass) => {
swiper.navigation.init();
swiper.navigation.update();
}, []);
return (
<div className={styles.sliderContainer}>
<Swiper
slidesPerView="auto"
spaceBetween={10}
slidesPerGroup={isSmallScreen ? 1 : 3}
navigation={true}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={handleSwiperInit}
>
{items.map((credit) => (
<SwiperSlide
key={credit.id}
style={{ width: posterWidth, height: rowHeight }}
>
<MovieCreditPoster
key={credit.id}
component={itemComponent}
posterWidth={posterWidth}
posterHeight={posterHeight}
tmdbId={credit.personTmdbId}
personName={credit.personName}
images={credit.images}
job={credit.job}
character={credit.character}
/>
</SwiperSlide>
))}
</Swiper>
</div>
);
}
export default MovieCreditPosters;
+34 -17
View File
@@ -20,12 +20,14 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import TraktRating from 'Components/TraktRating';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector'; import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import getMovieStatusDetails from 'Movie/getMovieStatusDetails'; import getMovieStatusDetails from 'Movie/getMovieStatusDetails';
import MovieHistoryModal from 'Movie/History/MovieHistoryModal'; import MovieHistoryModal from 'Movie/History/MovieHistoryModal';
import MovieCollectionLabelConnector from 'Movie/MovieCollectionLabelConnector';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal'; import MovieInteractiveSearchModal from 'Movie/Search/MovieInteractiveSearchModal';
import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable'; import MovieFileEditorTable from 'MovieFile/Editor/MovieFileEditorTable';
@@ -37,9 +39,8 @@ import * as keyCodes from 'Utilities/Constants/keyCodes';
import formatRuntime from 'Utilities/Date/formatRuntime'; import formatRuntime from 'Utilities/Date/formatRuntime';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector'; import MovieCastPosters from './Credits/Cast/MovieCastPosters';
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector'; import MovieCrewPosters from './Credits/Crew/MovieCrewPosters';
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
import MovieDetailsLinks from './MovieDetailsLinks'; import MovieDetailsLinks from './MovieDetailsLinks';
import MovieReleaseDates from './MovieReleaseDates'; import MovieReleaseDates from './MovieReleaseDates';
import MovieStatusLabel from './MovieStatusLabel'; import MovieStatusLabel from './MovieStatusLabel';
@@ -421,14 +422,15 @@ class MovieDetails extends Component {
<div className={styles.details}> <div className={styles.details}>
<div> <div>
{ {
!!certification && certification ?
<span className={styles.certification}> <span className={styles.certification}>
{certification} {certification}
</span> </span> :
null
} }
{ {
year > 0 && year > 0 ?
<span className={styles.year}> <span className={styles.year}>
<Popover <Popover
anchor={ anchor={
@@ -444,14 +446,16 @@ class MovieDetails extends Component {
} }
position={tooltipPositions.BOTTOM} position={tooltipPositions.BOTTOM}
/> />
</span> </span> :
null
} }
{ {
!!runtime && runtime ?
<span className={styles.runtime}> <span className={styles.runtime}>
{formatRuntime(runtime, movieRuntimeFormat)} {formatRuntime(runtime, movieRuntimeFormat)}
</span> </span> :
null
} }
{ {
@@ -497,31 +501,44 @@ class MovieDetails extends Component {
<div className={styles.details}> <div className={styles.details}>
{ {
!!ratings.tmdb && ratings.tmdb ?
<span className={styles.rating}> <span className={styles.rating}>
<TmdbRating <TmdbRating
ratings={ratings} ratings={ratings}
iconSize={20} iconSize={20}
/> />
</span> </span> :
null
} }
{ {
!!ratings.imdb && ratings.imdb ?
<span className={styles.rating}> <span className={styles.rating}>
<ImdbRating <ImdbRating
ratings={ratings} ratings={ratings}
iconSize={20} iconSize={20}
/> />
</span> </span> :
null
} }
{ {
!!ratings.rottenTomatoes && ratings.rottenTomatoes ?
<span className={styles.rating}> <span className={styles.rating}>
<RottenTomatoRating <RottenTomatoRating
ratings={ratings} ratings={ratings}
iconSize={20} iconSize={20}
/> />
</span> </span> :
null
}
{
ratings.trakt ?
<span className={styles.rating}>
<TraktRating
ratings={ratings}
iconSize={20}
/>
</span> :
null
} }
</div> </div>
@@ -685,13 +702,13 @@ class MovieDetails extends Component {
</FieldSet> </FieldSet>
<FieldSet legend={translate('Cast')}> <FieldSet legend={translate('Cast')}>
<MovieCastPostersConnector <MovieCastPosters
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
/> />
</FieldSet> </FieldSet>
<FieldSet legend={translate('Crew')}> <FieldSet legend={translate('Crew')}>
<MovieCrewPostersConnector <MovieCrewPosters
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
/> />
</FieldSet> </FieldSet>
@@ -2,23 +2,25 @@ import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import Movie from 'Movie/Movie';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDate from 'Utilities/Date/formatDate';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './MovieReleaseDates.css'; import styles from './MovieReleaseDates.css';
interface MovieReleaseDatesProps { type MovieReleaseDatesProps = Pick<
inCinemas?: string; Movie,
digitalRelease?: string; 'inCinemas' | 'digitalRelease' | 'physicalRelease'
physicalRelease?: string; >;
}
function MovieReleaseDates(props: MovieReleaseDatesProps) { function MovieReleaseDates({
const { inCinemas, digitalRelease, physicalRelease } = props; inCinemas,
digitalRelease,
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector( physicalRelease,
createUISettingsSelector() }: MovieReleaseDatesProps) {
); const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
useSelector(createUISettingsSelector());
if (!inCinemas && !physicalRelease && !digitalRelease) { if (!inCinemas && !physicalRelease && !digitalRelease) {
return ( return (
@@ -34,10 +36,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
return ( return (
<> <>
{inCinemas ? ( {inCinemas ? (
<div title={translate('InCinemas')}> <div
title={`${translate('InCinemas')}: ${formatDate(
inCinemas,
longDateFormat
)}`}
>
<div className={styles.dateIcon}> <div className={styles.dateIcon}>
<Icon name={icons.IN_CINEMAS} /> <Icon name={icons.IN_CINEMAS} />
</div> </div>
{getRelativeDate({ {getRelativeDate({
date: inCinemas, date: inCinemas,
shortDateFormat, shortDateFormat,
@@ -49,10 +57,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
) : null} ) : null}
{digitalRelease ? ( {digitalRelease ? (
<div title={translate('DigitalRelease')}> <div
title={`${translate('DigitalRelease')}: ${formatDate(
digitalRelease,
longDateFormat
)}`}
>
<div className={styles.dateIcon}> <div className={styles.dateIcon}>
<Icon name={icons.MOVIE_FILE} /> <Icon name={icons.MOVIE_FILE} />
</div> </div>
{getRelativeDate({ {getRelativeDate({
date: digitalRelease, date: digitalRelease,
shortDateFormat, shortDateFormat,
@@ -64,10 +78,16 @@ function MovieReleaseDates(props: MovieReleaseDatesProps) {
) : null} ) : null}
{physicalRelease ? ( {physicalRelease ? (
<div title={translate('PhysicalRelease')}> <div
title={`${translate('PhysicalRelease')}: ${formatDate(
physicalRelease,
longDateFormat
)}`}
>
<div className={styles.dateIcon}> <div className={styles.dateIcon}>
<Icon name={icons.DISC} /> <Icon name={icons.DISC} />
</div> </div>
{getRelativeDate({ {getRelativeDate({
date: physicalRelease, date: physicalRelease,
shortDateFormat, shortDateFormat,
@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import titleCase from 'Utilities/String/titleCase';
class MovieTitlesRow extends Component {
//
// Render
render() {
const {
title,
sourceType
} = this.props;
return (
<TableRow>
<TableRowCell>
{title}
</TableRowCell>
<TableRowCell>
{titleCase(sourceType)}
</TableRowCell>
</TableRow>
);
}
}
MovieTitlesRow.propTypes = {
id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
sourceType: PropTypes.string.isRequired
};
export default MovieTitlesRow;
@@ -0,0 +1,21 @@
import React from 'react';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import titleCase from 'Utilities/String/titleCase';
interface MovieTitlesRowProps {
title: string;
sourceType: string;
}
function MovieTitlesRow({ title, sourceType }: MovieTitlesRowProps) {
return (
<TableRow>
<TableRowCell>{title}</TableRowCell>
<TableRowCell>{titleCase(sourceType)}</TableRowCell>
</TableRow>
);
}
export default MovieTitlesRow;
@@ -1,3 +1,9 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
.container { .container {
border: 1px solid var(--borderColor); border: 1px solid var(--borderColor);
border-radius: 4px; border-radius: 4px;
@@ -1,6 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'blankpad': string;
'container': string; 'container': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
@@ -1,22 +0,0 @@
import React from 'react';
import MovieTitlesTableContentConnector from './MovieTitlesTableContentConnector';
import styles from './MovieTitlesTable.css';
function MovieTitlesTable(props) {
const {
...otherProps
} = props;
return (
<div className={styles.container}>
<MovieTitlesTableContentConnector
{...otherProps}
/>
</div>
);
}
MovieTitlesTable.propTypes = {
};
export default MovieTitlesTable;
@@ -0,0 +1,94 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Column from 'Components/Table/Column';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import sortByProp from 'Utilities/Array/sortByProp';
import translate from 'Utilities/String/translate';
import MovieTitlesRow from './MovieTitlesRow';
import styles from './MovieTitlesTable.css';
const columns: Column[] = [
{
name: 'alternativeTitle',
label: () => translate('AlternativeTitle'),
isVisible: true,
},
{
name: 'sourceType',
label: () => translate('Type'),
isVisible: true,
},
];
function movieAlternativeTitlesSelector(movieId: number) {
return createSelector(
(state: AppState) => state.movies,
(movies) => {
const { isFetching, isPopulated, error, items } = movies;
const alternateTitles =
items.find((m) => m.id === movieId)?.alternateTitles ?? [];
return {
isFetching,
isPopulated,
error,
items: alternateTitles,
};
}
);
}
interface MovieTitlesProps {
movieId: number;
}
function MovieTitlesTable({ movieId }: MovieTitlesProps) {
const { isFetching, isPopulated, error, items } = useSelector(
movieAlternativeTitlesSelector(movieId)
);
const sortedItems = items.sort(sortByProp('title'));
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>
{translate('AlternativeTitlesLoadError')}
</Alert>
);
}
return (
<div className={styles.container}>
{isFetching && <LoadingIndicator />}
{isPopulated && !items.length && !error ? (
<div className={styles.blankpad}>
{translate('NoAlternativeTitles')}
</div>
) : null}
{isPopulated && !!items.length && !error ? (
<Table columns={columns}>
<TableBody>
{sortedItems.map((item) => (
<MovieTitlesRow
key={item.id}
title={item.title}
sourceType={item.sourceType}
/>
))}
</TableBody>
</Table>
) : null}
</div>
);
}
export default MovieTitlesTable;
@@ -1,5 +0,0 @@
.blankpad {
padding-top: 10px;
padding-bottom: 10px;
padding-left: 2em;
}
@@ -1,87 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import translate from 'Utilities/String/translate';
import MovieTitlesRow from './MovieTitlesRow';
import styles from './MovieTitlesTableContent.css';
const columns = [
{
name: 'altTitle',
label: () => translate('AlternativeTitle'),
isVisible: true
},
{
name: 'sourceType',
label: () => translate('Type'),
isVisible: true
}
];
class MovieTitlesTableContent extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items
} = this.props;
const hasItems = !!items.length;
return (
<div>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div className={styles.blankpad}>
{translate('UnableToLoadAltTitle')}
</div>
}
{
isPopulated && !hasItems && !error &&
<div className={styles.blankpad}>
{translate('NoAltTitle')}
</div>
}
{
isPopulated && hasItems && !error &&
<Table columns={columns}>
<TableBody>
{
items.reverse().map((item) => {
return (
<MovieTitlesRow
key={item.id}
{...item}
/>
);
})
}
</TableBody>
</Table>
}
</div>
);
}
}
MovieTitlesTableContent.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired
};
export default MovieTitlesTableContent;
@@ -1,60 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import MovieTitlesTableContent from './MovieTitlesTableContent';
function createMapStateToProps() {
return createSelector(
(state, { movieId }) => movieId,
(state) => state.movies,
(movieId, movies) => {
const {
isFetching,
isPopulated,
error,
items
} = movies;
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
return {
isFetching,
isPopulated,
error,
alternateTitles
};
}
);
}
class MovieTitlesTableContentConnector extends Component {
//
// Render
render() {
const {
alternateTitles,
...otherProps
} = this.props;
return (
<MovieTitlesTableContent
{...otherProps}
items={alternateTitles}
/>
);
}
}
MovieTitlesTableContentConnector.propTypes = {
movieId: PropTypes.number.isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
};
MovieTitlesTableContentConnector.defaultProps = {
alternateTitles: []
};
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);
@@ -4,8 +4,6 @@
margin-right: auto; margin-right: auto;
} }
.tagInternalInput { .labelIcon {
composes: internalInput from '~Components/Form/TagInput.css'; margin-left: 8px;
flex: 0 0 100%;
} }
+1 -1
View File
@@ -2,7 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'deleteButton': string; 'deleteButton': string;
'tagInternalInput': string; 'labelIcon': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,16 +1,19 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal'; import MoveMovieModal from 'Movie/MoveMovie/MoveMovieModal';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './EditMovieModalContent.css'; import styles from './EditMovieModalContent.css';
@@ -103,7 +106,21 @@ class EditMovieModalContent extends Component {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel> <FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.AVAILABILITY_SELECT} type={inputTypes.AVAILABILITY_SELECT}
@@ -136,6 +136,15 @@ function MovieIndexSortMenu(props: MovieIndexSortMenuProps) {
{translate('RottenTomatoesRating')} {translate('RottenTomatoesRating')}
</SortMenuItem> </SortMenuItem>
<SortMenuItem
name="traktRating"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('TraktRating')}
</SortMenuItem>
<SortMenuItem <SortMenuItem
name="popularity" name="popularity"
sortKey={sortKey} sortKey={sortKey}
@@ -11,6 +11,7 @@ import RottenTomatoRating from 'Components/RottenTomatoRating';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import Popover from 'Components/Tooltip/Popover'; import Popover from 'Components/Tooltip/Popover';
import TraktRating from 'Components/TraktRating';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
@@ -21,6 +22,7 @@ import { Statistics } from 'Movie/Movie';
import MoviePoster from 'Movie/MoviePoster'; import MoviePoster from 'Movie/MoviePoster';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDate from 'Utilities/Date/formatDate';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import createMovieIndexItemSelector from '../createMovieIndexItemSelector'; import createMovieIndexItemSelector from '../createMovieIndexItemSelector';
@@ -54,6 +56,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating, showRottenTomatoesRating,
showTraktRating,
showTags, showTags,
showSearchAction, showSearchAction,
} = useSelector(selectPosterOptions); } = useSelector(selectPosterOptions);
@@ -241,7 +244,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
) : null} ) : null}
{showCinemaRelease && inCinemas ? ( {showCinemaRelease && inCinemas ? (
<div className={styles.title} title={translate('InCinemas')}> <div
className={styles.title}
title={`${translate('InCinemas')}: ${formatDate(
inCinemas,
longDateFormat
)}`}
>
<Icon name={icons.IN_CINEMAS} />{' '} <Icon name={icons.IN_CINEMAS} />{' '}
{getRelativeDate({ {getRelativeDate({
date: inCinemas, date: inCinemas,
@@ -254,7 +263,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
) : null} ) : null}
{showDigitalRelease && digitalRelease ? ( {showDigitalRelease && digitalRelease ? (
<div className={styles.title} title={translate('DigitalRelease')}> <div
className={styles.title}
title={`${translate('DigitalRelease')}: ${formatDate(
digitalRelease,
longDateFormat
)}`}
>
<Icon name={icons.MOVIE_FILE} />{' '} <Icon name={icons.MOVIE_FILE} />{' '}
{getRelativeDate({ {getRelativeDate({
date: digitalRelease, date: digitalRelease,
@@ -267,7 +282,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
) : null} ) : null}
{showPhysicalRelease && physicalRelease ? ( {showPhysicalRelease && physicalRelease ? (
<div className={styles.title} title={translate('PhysicalRelease')}> <div
className={styles.title}
title={`${translate('PhysicalRelease')}: ${formatDate(
physicalRelease,
longDateFormat
)}`}
>
<Icon name={icons.DISC} />{' '} <Icon name={icons.DISC} />{' '}
{getRelativeDate({ {getRelativeDate({
date: physicalRelease, date: physicalRelease,
@@ -280,7 +301,13 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
) : null} ) : null}
{showReleaseDate && releaseDate ? ( {showReleaseDate && releaseDate ? (
<div className={styles.title} title={translate('ReleaseDate')}> <div
className={styles.title}
title={`${translate('ReleaseDate')}: ${formatDate(
releaseDate,
longDateFormat
)}`}
>
<Icon name={icons.CALENDAR} />{' '} <Icon name={icons.CALENDAR} />{' '}
{getRelativeDate({ {getRelativeDate({
date: releaseDate, date: releaseDate,
@@ -310,6 +337,12 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
</div> </div>
) : null} ) : null}
{showTraktRating && !!ratings.trakt ? (
<div className={styles.title}>
<TraktRating ratings={ratings} iconSize={12} />
</div>
) : null}
{showTags && tags.length ? ( {showTags && tags.length ? (
<div className={styles.tags}> <div className={styles.tags}>
<div className={styles.tagsList}> <div className={styles.tagsList}>
@@ -347,6 +380,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
showTmdbRating={showTmdbRating} showTmdbRating={showTmdbRating}
showImdbRating={showImdbRating} showImdbRating={showImdbRating}
showRottenTomatoesRating={showRottenTomatoesRating} showRottenTomatoesRating={showRottenTomatoesRating}
showTraktRating={showTraktRating}
showTags={showTags} showTags={showTags}
/> />
@@ -4,10 +4,12 @@ import ImdbRating from 'Components/ImdbRating';
import RottenTomatoRating from 'Components/RottenTomatoRating'; import RottenTomatoRating from 'Components/RottenTomatoRating';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import TraktRating from 'Components/TraktRating';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import Language from 'Language/Language'; import Language from 'Language/Language';
import { Ratings } from 'Movie/Movie'; import { Ratings } from 'Movie/Movie';
import QualityProfile from 'typings/QualityProfile'; import QualityProfile from 'typings/QualityProfile';
import formatDate from 'Utilities/Date/formatDate';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
@@ -43,6 +45,7 @@ interface MovieIndexPosterInfoProps {
showTmdbRating: boolean; showTmdbRating: boolean;
showImdbRating: boolean; showImdbRating: boolean;
showRottenTomatoesRating: boolean; showRottenTomatoesRating: boolean;
showTraktRating: boolean;
showTags: boolean; showTags: boolean;
} }
@@ -76,6 +79,7 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating, showRottenTomatoesRating,
showTraktRating,
showTags, showTags,
} = props; } = props;
@@ -136,7 +140,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
}); });
return ( return (
<div className={styles.info} title={translate('InCinemas')}> <div
className={styles.info}
title={`${translate('InCinemas')}: ${formatDate(
inCinemas,
longDateFormat
)}`}
>
<Icon name={icons.IN_CINEMAS} /> {inCinemasDate} <Icon name={icons.IN_CINEMAS} /> {inCinemasDate}
</div> </div>
); );
@@ -152,7 +162,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
}); });
return ( return (
<div className={styles.info} title={translate('DigitalRelease')}> <div
className={styles.info}
title={`${translate('DigitalRelease')}: ${formatDate(
digitalRelease,
longDateFormat
)}`}
>
<Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate} <Icon name={icons.MOVIE_FILE} /> {digitalReleaseDate}
</div> </div>
); );
@@ -172,7 +188,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
}); });
return ( return (
<div className={styles.info} title={translate('PhysicalRelease')}> <div
className={styles.info}
title={`${translate('PhysicalRelease')}: ${formatDate(
physicalRelease,
longDateFormat
)}`}
>
<Icon name={icons.DISC} /> {physicalReleaseDate} <Icon name={icons.DISC} /> {physicalReleaseDate}
</div> </div>
); );
@@ -180,7 +202,13 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
if (sortKey === 'releaseDate' && releaseDate && !showReleaseDate) { if (sortKey === 'releaseDate' && releaseDate && !showReleaseDate) {
return ( return (
<div className={styles.info} title={translate('ReleaseDate')}> <div
className={styles.info}
title={`${translate('ReleaseDate')}: ${formatDate(
releaseDate,
longDateFormat
)}`}
>
<Icon name={icons.CALENDAR} />{' '} <Icon name={icons.CALENDAR} />{' '}
{getRelativeDate({ {getRelativeDate({
date: releaseDate, date: releaseDate,
@@ -221,6 +249,14 @@ function MovieIndexPosterInfo(props: MovieIndexPosterInfoProps) {
); );
} }
if (!showTraktRating && sortKey === 'traktRating' && !!ratings.trakt) {
return (
<div className={styles.info}>
<TraktRating ratings={ratings} iconSize={12} />
</div>
);
}
if (!showTags && sortKey === 'tags' && tags.length) { if (!showTags && sortKey === 'tags' && tags.length) {
return ( return (
<div className={styles.tags}> <div className={styles.tags}>
@@ -150,6 +150,7 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating, showRottenTomatoesRating,
showTraktRating,
showTags, showTags,
} = posterOptions; } = posterOptions;
@@ -199,6 +200,10 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
heights.push(19); heights.push(19);
} }
if (showTraktRating) {
heights.push(19);
}
if (showTags) { if (showTags) {
heights.push(21); heights.push(21);
} }
@@ -253,6 +258,11 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
heights.push(19); heights.push(19);
} }
break; break;
case 'traktRating':
if (!showTraktRating) {
heights.push(19);
}
break;
case 'tags': case 'tags':
if (!showTags) { if (!showTags) {
heights.push(21); heights.push(21);
@@ -59,6 +59,7 @@ function MovieIndexPosterOptionsModalContent(
showTmdbRating, showTmdbRating,
showImdbRating, showImdbRating,
showRottenTomatoesRating, showRottenTomatoesRating,
showTraktRating,
showTags, showTags,
showSearchAction, showSearchAction,
} = posterOptions; } = posterOptions;
@@ -222,6 +223,18 @@ function MovieIndexPosterOptionsModalContent(
/> />
</FormGroup> </FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ShowTraktRating')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showTraktRating"
value={showTraktRating}
helpText={translate('ShowTraktRatingPosterHelpText')}
onChange={onPosterOptionChange}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}> <FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ShowTags')}</FormLabel> <FormLabel>{translate('ShowTags')}</FormLabel>
@@ -92,7 +92,8 @@
.imdbRating, .imdbRating,
.tmdbRating, .tmdbRating,
.rottenTomatoesRating { .rottenTomatoesRating,
.traktRating {
composes: cell; composes: cell;
flex: 0 0 80px; flex: 0 0 80px;
+1
View File
@@ -30,6 +30,7 @@ interface CssExports {
'studio': string; 'studio': string;
'tags': string; 'tags': string;
'tmdbRating': string; 'tmdbRating': string;
'traktRating': string;
'year': string; 'year': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
@@ -14,6 +14,7 @@ import Column from 'Components/Table/Column';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import TmdbRating from 'Components/TmdbRating'; import TmdbRating from 'Components/TmdbRating';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import TraktRating from 'Components/TraktRating';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal'; import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
@@ -387,6 +388,14 @@ function MovieIndexRow(props: MovieIndexRowProps) {
); );
} }
if (name === 'traktRating') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{ratings.trakt ? <TraktRating ratings={ratings} /> : null}
</VirtualTableRowCell>
);
}
if (name === 'popularity') { if (name === 'popularity') {
return ( return (
<VirtualTableRowCell key={name} className={styles[name]}> <VirtualTableRowCell key={name} className={styles[name]}>
@@ -82,7 +82,8 @@
.imdbRating, .imdbRating,
.tmdbRating, .tmdbRating,
.rottenTomatoesRating { .rottenTomatoesRating,
.traktRating {
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css'; composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 0 80px; flex: 0 0 80px;
@@ -27,6 +27,7 @@ interface CssExports {
'studio': string; 'studio': string;
'tags': string; 'tags': string;
'tmdbRating': string; 'tmdbRating': string;
'traktRating': string;
'year': string; 'year': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
+9 -1
View File
@@ -9,7 +9,7 @@ export type MovieStatus =
| 'released' | 'released'
| 'deleted'; | 'deleted';
export type CoverType = 'poster' | 'fanart'; export type CoverType = 'poster' | 'fanart' | 'headshot';
export interface Image { export interface Image {
coverType: CoverType; coverType: CoverType;
@@ -37,6 +37,12 @@ export interface Ratings {
tmdb: RatingValues; tmdb: RatingValues;
metacritic: RatingValues; metacritic: RatingValues;
rottenTomatoes: RatingValues; rottenTomatoes: RatingValues;
trakt: RatingValues;
}
export interface AlternativeTitle extends ModelBase {
sourceType: string;
title: string;
} }
interface Movie extends ModelBase { interface Movie extends ModelBase {
@@ -52,6 +58,7 @@ interface Movie extends ModelBase {
originalTitle: string; originalTitle: string;
originalLanguage: Language; originalLanguage: Language;
collection: Collection; collection: Collection;
alternateTitles: AlternativeTitle[];
studio: string; studio: string;
qualityProfileId: number; qualityProfileId: number;
added: string; added: string;
@@ -72,6 +79,7 @@ interface Movie extends ModelBase {
images: Image[]; images: Image[];
movieFile: MovieFile; movieFile: MovieFile;
hasFile: boolean; hasFile: boolean;
lastSearchTime?: string;
isAvailable: boolean; isAvailable: boolean;
isSaving?: boolean; isSaving?: boolean;
} }
-25
View File
@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import MovieImage from './MovieImage';
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
function MovieHeadshot(props) {
return (
<MovieImage
{...props}
coverType="headshot"
placeholder={posterPlaceholder}
/>
);
}
MovieHeadshot.propTypes = {
size: PropTypes.number.isRequired
};
MovieHeadshot.defaultProps = {
size: 250
};
export default MovieHeadshot;
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import MovieImage, { MovieImageProps } from './MovieImage';
const posterPlaceholder =
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKgAAAD3CAMAAAC+Te+kAAAAZlBMVEUvLi8vLy8vLzAvMDAwLy8wLzAwMDAwMDEwMTExMDAxMDExMTExMTIxMjIyMjIyMjMyMzMzMjMzMzMzMzQzNDQ0NDQ0NDU0NTU1NTU1NTY1NjY2NTY2NjY2Njc2Nzc3Njc3Nzc3NziHChLWAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+MKCgEdHeShUbsAAALZSURBVHja7dxNcuwgDEZR1qAVmP1vMrNUJe91GfTzCSpXo575lAymjYWGXRIDKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKFCgQIECBQoUKNA/AZ3fcTR0/owjofNDnAadnwPoPnS+xTXQeQZ0rkQ/dC4H0Gzo7ITO3bgGOnug/2PcAF3Mczt0fUj0QncG7znQBupw3PkWqh8qpkagpnyqjuArkkxaC02kRqGypCZANVYFdJZCdy9WTRVB5znQ6qTmjFFBWnOhdg20Lqnp0CpqAbRmAJRAK5JaA32zngTNvv910OSkVkJTs1oLtWugeTkNQZ/nkT2rotBHldUwNE6VQTVWGTQ6AHKggqGaBS23JkKf0hUgE1qa01Ro5fzPhoapR0HtCGg4q0poSCqFRgaAFhqxqqEr1EOgmdJaqHdaHQq1I6CunPZAHdY2aIJUBN2V9kE3H1Wd0BXrNVA7BLpgdUCtALo8pZqhdgd0Z6OyE7q1pdoH3dv7tS7o7iZ1E3R/N70Huuz795cQao65vvkqooT+vEgDdPcbj2s3zxTv9Qt/7cuhdgfUo2yAOplyqNuphfqZSqhFmEJo0HkcdPZCo0rRymRxpwSawHR+YtyBZihfvi+nQO0OqCmcYahGqYPGS4qCUJkzBpUpJdCkordyaFZxXi1UUpaZAJ2XQFOLh8ug2XXjVdD0+vYiqLIO3w1VH8EogtoxUPnpGxe04zyTA1p57i4T2nTmbnnnUuLMg1afYE2C1h+1zYEKjlknQLtPg9tb3YzU+dL054qOBb8cvcz3DlqBZhUmhdrnKo9j+pR0rkN5UHkznZHPtJIYN2TTCe1poTUyk9nWPO0bt8Ys7Ug34mlUMONtPUXMaEdXnXN1MnUzN2Z9q3Lr8XQN1DaLQJpXpiamZwltYdIUHShQoECBAgUKFChQoECBAgUKFChQoECBAgUKFChQoECBAgUKFCjQ+vgCff/mEp/vtiIAAAAASUVORK5CYII=';
interface MovieHeadshotProps
extends Omit<MovieImageProps, 'coverType' | 'placeholder'> {
size?: 250 | 500;
}
function MovieHeadshot({ size = 250, ...otherProps }: MovieHeadshotProps) {
return (
<MovieImage
{...otherProps}
size={size}
coverType="headshot"
placeholder={posterPlaceholder}
/>
);
}
export default MovieHeadshot;
+1 -1
View File
@@ -43,7 +43,7 @@ function MovieImage({
}: MovieImageProps) { }: MovieImageProps) {
const [url, setUrl] = useState<string | null>(null); const [url, setUrl] = useState<string | null>(null);
const [hasError, setHasError] = useState(false); const [hasError, setHasError] = useState(false);
const [isLoaded, setIsLoaded] = useState(false); const [isLoaded, setIsLoaded] = useState(true);
const image = useRef<Image | null>(null); const image = useRef<Image | null>(null);
const handleLoad = useCallback(() => { const handleLoad = useCallback(() => {
@@ -19,7 +19,7 @@ function EditImportListExclusionModal(
const dispatch = useDispatch(); const dispatch = useDispatch();
const onModalClosePress = useCallback(() => { const handleModalClose = useCallback(() => {
dispatch( dispatch(
clearPendingChanges({ clearPendingChanges({
section: 'settings.importListExclusions', section: 'settings.importListExclusions',
@@ -29,10 +29,10 @@ function EditImportListExclusionModal(
}, [dispatch, onModalClose]); }, [dispatch, onModalClose]);
return ( return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClosePress}> <Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={handleModalClose}>
<EditImportListExclusionModalContent <EditImportListExclusionModalContent
{...otherProps} {...otherProps}
onModalClose={onModalClosePress} onModalClose={handleModalClose}
/> />
</Modal> </Modal>
); );
@@ -32,12 +32,6 @@ const newImportListExclusion = {
tmdbId: 0, tmdbId: 0,
}; };
interface EditImportListExclusionModalContentProps {
id?: number;
onModalClose: () => void;
onDeleteImportListExclusionPress?: () => void;
}
function createImportListExclusionSelector(id?: number) { function createImportListExclusionSelector(id?: number) {
return createSelector( return createSelector(
(state: AppState) => state.settings.importListExclusions, (state: AppState) => state.settings.importListExclusions,
@@ -63,12 +57,24 @@ function createImportListExclusionSelector(id?: number) {
); );
} }
function EditImportListExclusionModalContent( interface EditImportListExclusionModalContentProps {
props: EditImportListExclusionModalContentProps id?: number;
) { onModalClose: () => void;
const { id, onModalClose, onDeleteImportListExclusionPress } = props; onDeleteImportListExclusionPress?: () => void;
}
function EditImportListExclusionModalContent({
id,
onModalClose,
onDeleteImportListExclusionPress,
}: EditImportListExclusionModalContentProps) {
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
useSelector(createImportListExclusionSelector(id));
const { movieTitle, movieYear, tmdbId } = item;
const dispatch = useDispatch(); const dispatch = useDispatch();
const previousIsSaving = usePrevious(isSaving);
const dispatchSetImportListExclusionValue = (payload: { const dispatchSetImportListExclusionValue = (payload: {
name: string; name: string;
@@ -78,20 +84,10 @@ function EditImportListExclusionModalContent(
dispatch(setImportListExclusionValue(payload)); dispatch(setImportListExclusionValue(payload));
}; };
const { isFetching, isSaving, item, error, saveError, ...otherProps } =
useSelector(createImportListExclusionSelector(props.id));
const previousIsSaving = usePrevious(isSaving);
const { movieTitle, movieYear, tmdbId } = item;
useEffect(() => { useEffect(() => {
if (!id) { if (!id) {
Object.keys(newImportListExclusion).forEach((name) => { Object.entries(newImportListExclusion).forEach(([name, value]) => {
dispatchSetImportListExclusionValue({ dispatchSetImportListExclusionValue({ name, value });
name,
value:
newImportListExclusion[name as keyof typeof newImportListExclusion],
});
}); });
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -101,7 +97,7 @@ function EditImportListExclusionModalContent(
if (previousIsSaving && !isSaving && !saveError) { if (previousIsSaving && !isSaving && !saveError) {
onModalClose(); onModalClose();
} }
}); }, [previousIsSaving, isSaving, saveError, onModalClose]);
const onSavePress = useCallback(() => { const onSavePress = useCallback(() => {
dispatch(saveImportListExclusion({ id })); dispatch(saveImportListExclusion({ id }));
@@ -42,7 +42,7 @@ import styles from './ImportListExclusions.css';
const COLUMNS: Column[] = [ const COLUMNS: Column[] = [
{ {
name: 'tmdbid', name: 'tmdbId',
label: () => translate('TMDBId'), label: () => translate('TMDBId'),
isVisible: true, isVisible: true,
isSortable: true, isSortable: true,
@@ -4,6 +4,10 @@
margin-right: auto; margin-right: auto;
} }
.labelIcon {
margin-left: 8px;
}
.message { .message {
composes: alert from '~Components/Alert.css'; composes: alert from '~Components/Alert.css';
@@ -2,6 +2,7 @@
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'deleteButton': string; 'deleteButton': string;
'labelIcon': string;
'message': string; 'message': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
@@ -1,11 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import MovieMinimumAvailabilityPopoverContent from 'AddMovie/MovieMinimumAvailabilityPopoverContent';
import Alert from 'Components/Alert'; import Alert from 'Components/Alert';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup'; import ProviderFieldFormGroup from 'Components/Form/ProviderFieldFormGroup';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton'; import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -13,7 +15,8 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton'; import AdvancedSettingsButton from 'Settings/AdvancedSettingsButton';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
@@ -160,12 +163,28 @@ function EditImportListModalContent(props) {
</FormGroup> </FormGroup>
<FormGroup> <FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel> <FormLabel>
{translate('MinimumAvailability')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MinimumAvailability')}
body={<MovieMinimumAvailabilityPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.AVAILABILITY_SELECT} type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability" name="minimumAvailability"
{...minimumAvailability} {...minimumAvailability}
onChange={onInputChange} onChange={onInputChange}
helpLink="https://wiki.servarr.com/radarr/faq#what-is-minimum-availability"
/> />
</FormGroup> </FormGroup>
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
import Card from 'Components/Card'; import Card from 'Components/Card';
import Label from 'Components/Label'; import Label from 'Components/Label';
import ConfirmModal from 'Components/Modal/ConfirmModal'; import ConfirmModal from 'Components/Modal/ConfirmModal';
import TagList from 'Components/TagList';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan'; import formatShortTimeSpan from 'Utilities/Date/formatShortTimeSpan';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
@@ -58,6 +59,8 @@ class ImportList extends Component {
name, name,
enabled, enabled,
enableAuto, enableAuto,
tags,
tagList,
minRefreshInterval minRefreshInterval
} = this.props; } = this.props;
@@ -72,7 +75,6 @@ class ImportList extends Component {
</div> </div>
<div className={styles.enabled}> <div className={styles.enabled}>
{ {
enabled ? enabled ?
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
@@ -87,15 +89,21 @@ class ImportList extends Component {
} }
{ {
enableAuto && enableAuto ?
<Label kind={kinds.SUCCESS}> <Label kind={kinds.SUCCESS}>
{translate('AutomaticAdd')} {translate('AutomaticAdd')}
</Label> </Label> :
null
} }
</div> </div>
<TagList
tags={tags}
tagList={tagList}
/>
<div className={styles.enabled}> <div className={styles.enabled}>
<Label kind={kinds.INFO} title='List Refresh Interval'> <Label kind={kinds.DEFAULT} title='List Refresh Interval'>
{`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`} {`${translate('Refresh')}: ${formatShortTimeSpan(minRefreshInterval)}`}
</Label> </Label>
</div> </div>
@@ -126,6 +134,8 @@ ImportList.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
enabled: PropTypes.bool.isRequired, enabled: PropTypes.bool.isRequired,
enableAuto: PropTypes.bool.isRequired, enableAuto: PropTypes.bool.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
minRefreshInterval: PropTypes.string.isRequired, minRefreshInterval: PropTypes.string.isRequired,
onConfirmDeleteImportList: PropTypes.func.isRequired onConfirmDeleteImportList: PropTypes.func.isRequired
}; };
@@ -49,6 +49,7 @@ class ImportLists extends Component {
render() { render() {
const { const {
items, items,
tagList,
onConfirmDeleteImportList, onConfirmDeleteImportList,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -71,6 +72,7 @@ class ImportLists extends Component {
<ImportList <ImportList
key={item.id} key={item.id}
{...item} {...item}
tagList={tagList}
onConfirmDeleteImportList={onConfirmDeleteImportList} onConfirmDeleteImportList={onConfirmDeleteImportList}
/> />
); );
@@ -109,6 +111,7 @@ ImportLists.propTypes = {
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
tagList: PropTypes.arrayOf(PropTypes.object).isRequired,
onConfirmDeleteImportList: PropTypes.func.isRequired onConfirmDeleteImportList: PropTypes.func.isRequired
}; };
@@ -5,13 +5,20 @@ import { createSelector } from 'reselect';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions'; import { deleteImportList, fetchImportLists } from 'Store/Actions/settingsActions';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector'; import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import sortByProp from 'Utilities/Array/sortByProp'; import sortByProp from 'Utilities/Array/sortByProp';
import ImportLists from './ImportLists'; import ImportLists from './ImportLists';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createSortedSectionSelector('settings.importLists', sortByProp('name')), createSortedSectionSelector('settings.importLists', sortByProp('name')),
(importLists) => importLists createTagsSelector(),
(importLists, tagList) => {
return {
...importLists,
tagList
};
}
); );
} }
@@ -13,7 +13,7 @@ import { inputTypes, kinds, sizes } from 'Helpers/Props';
import RootFolders from 'RootFolder/RootFolders'; import RootFolders from 'RootFolder/RootFolders';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector'; import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import NamingConnector from './Naming/NamingConnector'; import Naming from './Naming/Naming';
import AddRootFolder from './RootFolder/AddRootFolder'; import AddRootFolder from './RootFolder/AddRootFolder';
const rescanAfterRefreshOptions = [ const rescanAfterRefreshOptions = [
@@ -106,7 +106,7 @@ class MediaManagement extends Component {
/> />
<PageContentBody> <PageContentBody>
<NamingConnector /> <Naming />
{ {
isFetching ? isFetching ?
@@ -174,24 +174,21 @@ class MediaManagement extends Component {
<FieldSet <FieldSet
legend={translate('Importing')} legend={translate('Importing')}
> >
{ <FormGroup
!isWindows && advancedSettings={advancedSettings}
<FormGroup isAdvanced={true}
advancedSettings={advancedSettings} size={sizes.MEDIUM}
isAdvanced={true} >
size={sizes.MEDIUM} <FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
>
<FormLabel>{translate('SkipFreeSpaceCheck')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="skipFreeSpaceCheckWhenImporting" name="skipFreeSpaceCheckWhenImporting"
helpText={translate('SkipFreeSpaceCheckWhenImportingHelpText')} helpText={translate('SkipFreeSpaceCheckHelpText')}
onChange={onInputChange} onChange={onInputChange}
{...settings.skipFreeSpaceCheckWhenImporting} {...settings.skipFreeSpaceCheckWhenImporting}
/> />
</FormGroup> </FormGroup>
}
<FormGroup <FormGroup
advancedSettings={advancedSettings} advancedSettings={advancedSettings}
@@ -1,252 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import NamingModal from './NamingModal';
import styles from './Naming.css';
const colonReplacementOptions = [
{
key: 'delete',
get value() {
return translate('Delete');
}
},
{
key: 'dash',
get value() {
return translate('ReplaceWithDash');
}
},
{
key: 'spaceDash',
get value() {
return translate('ReplaceWithSpaceDash');
}
},
{
key: 'spaceDashSpace',
get value() {
return translate('ReplaceWithSpaceDashSpace');
}
}
];
class Naming extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isNamingModalOpen: false,
namingModalOptions: null
};
}
//
// Listeners
onStandardNamingModalOpenClick = () => {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'standardMovieFormat',
additional: true
}
});
};
onMovieFolderNamingModalOpenClick = () => {
this.setState({
isNamingModalOpen: true,
namingModalOptions: {
name: 'movieFolderFormat'
}
});
};
onNamingModalClose = () => {
this.setState({ isNamingModalOpen: false });
};
//
// Render
render() {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
examples,
examplesPopulated,
onInputChange
} = this.props;
const {
isNamingModalOpen,
namingModalOptions
} = this.state;
const renameMovies = hasSettings && settings.renameMovies.value;
const replaceIllegalCharacters = hasSettings && settings.replaceIllegalCharacters.value;
const standardMovieFormatHelpTexts = [];
const standardMovieFormatErrors = [];
const movieFolderFormatHelpTexts = [];
const movieFolderFormatErrors = [];
if (examplesPopulated) {
if (examples.movieExample) {
standardMovieFormatHelpTexts.push(`${translate('Movie')}: ${examples.movieExample}`);
} else {
standardMovieFormatErrors.push({ get message() {
return translate('MovieInvalidFormat');
} });
}
if (examples.movieFolderExample) {
movieFolderFormatHelpTexts.push(`${translate('Example')}: ${examples.movieFolderExample}`);
} else {
movieFolderFormatErrors.push({ get message() {
return translate('InvalidFormat');
} });
}
}
return (
<FieldSet legend={translate('MovieNaming')}>
{
isFetching &&
<LoadingIndicator />
}
{
!isFetching && error &&
<Alert kind={kinds.DANGER}>
{translate('NamingSettingsLoadError')}
</Alert>
}
{
hasSettings && !isFetching && !error &&
<Form>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('RenameMovies')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="renameMovies"
helpText={translate('RenameMoviesHelpText')}
onChange={onInputChange}
{...settings.renameMovies}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ReplaceIllegalCharacters')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="replaceIllegalCharacters"
helpText={translate('ReplaceIllegalCharactersHelpText')}
onChange={onInputChange}
{...settings.replaceIllegalCharacters}
/>
</FormGroup>
{
replaceIllegalCharacters &&
<FormGroup>
<FormLabel>{translate('ColonReplacement')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="colonReplacementFormat"
values={colonReplacementOptions}
helpText={translate('ColonReplacementFormatHelpText')}
onChange={onInputChange}
{...settings.colonReplacementFormat}
/>
</FormGroup>
}
{
renameMovies &&
<FormGroup size={sizes.LARGE}>
<FormLabel>{translate('StandardMovieFormat')}</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="standardMovieFormat"
buttons={<FormInputButton onPress={this.onStandardNamingModalOpenClick}>?</FormInputButton>}
onChange={onInputChange}
{...settings.standardMovieFormat}
helpTexts={standardMovieFormatHelpTexts}
errors={[...standardMovieFormatErrors, ...settings.standardMovieFormat.errors]}
/>
</FormGroup>
}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
>
<FormLabel>{translate('MovieFolderFormat')}</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="movieFolderFormat"
buttons={<FormInputButton onPress={this.onMovieFolderNamingModalOpenClick}>?</FormInputButton>}
onChange={onInputChange}
{...settings.movieFolderFormat}
helpTexts={[translate('MovieFolderFormatHelpText'), ...movieFolderFormatHelpTexts]}
errors={[...movieFolderFormatErrors, ...settings.movieFolderFormat.errors]}
/>
</FormGroup>
{
namingModalOptions &&
<NamingModal
isOpen={isNamingModalOpen}
advancedSettings={advancedSettings}
{...namingModalOptions}
value={settings[namingModalOptions.name].value}
onInputChange={onInputChange}
onModalClose={this.onNamingModalClose}
/>
}
</Form>
}
</FieldSet>
);
}
}
Naming.propTypes = {
advancedSettings: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
settings: PropTypes.object.isRequired,
hasSettings: PropTypes.bool.isRequired,
examples: PropTypes.object.isRequired,
examplesPopulated: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired
};
export default Naming;
@@ -0,0 +1,273 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
import useModalOpenState from 'Helpers/Hooks/useModalOpenState';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import {
fetchNamingExamples,
fetchNamingSettings,
setNamingSettingsValue,
} from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import NamingConfig from 'typings/Settings/NamingConfig';
import translate from 'Utilities/String/translate';
import NamingModal from './NamingModal';
import styles from './Naming.css';
const SECTION = 'naming';
function createNamingSelector() {
return createSelector(
(state: AppState) => state.settings.advancedSettings,
(state: AppState) => state.settings.namingExamples,
createSettingsSectionSelector(SECTION),
(advancedSettings, namingExamples, sectionSettings) => {
return {
advancedSettings,
examples: namingExamples.item,
examplesPopulated: namingExamples.isPopulated,
...sectionSettings,
};
}
);
}
interface NamingModalOptions {
name: keyof Pick<NamingConfig, 'standardMovieFormat' | 'movieFolderFormat'>;
movie?: boolean;
additional?: boolean;
}
function Naming() {
const {
advancedSettings,
isFetching,
error,
settings,
hasSettings,
examples,
examplesPopulated,
} = useSelector(createNamingSelector());
const dispatch = useDispatch();
const [isNamingModalOpen, setNamingModalOpen, setNamingModalClosed] =
useModalOpenState(false);
const [namingModalOptions, setNamingModalOptions] =
useState<NamingModalOptions | null>(null);
const namingExampleTimeout = useRef<ReturnType<typeof setTimeout>>();
useEffect(() => {
dispatch(fetchNamingSettings());
dispatch(fetchNamingExamples());
return () => {
dispatch(clearPendingChanges({ section: SECTION }));
};
}, [dispatch]);
const handleInputChange = useCallback(
({ name, value }: { name: string; value: string }) => {
// @ts-expect-error 'setNamingSettingsValue' isn't typed yet
dispatch(setNamingSettingsValue({ name, value }));
if (namingExampleTimeout.current) {
clearTimeout(namingExampleTimeout.current);
}
namingExampleTimeout.current = setTimeout(() => {
dispatch(fetchNamingExamples());
}, 1000);
},
[dispatch]
);
const onStandardNamingModalOpenClick = useCallback(() => {
setNamingModalOpen();
setNamingModalOptions({
name: 'standardMovieFormat',
movie: true,
additional: true,
});
}, [setNamingModalOpen, setNamingModalOptions]);
const onMovieFolderNamingModalOpenClick = useCallback(() => {
setNamingModalOpen();
setNamingModalOptions({
name: 'movieFolderFormat',
});
}, [setNamingModalOpen, setNamingModalOptions]);
const renameMovies = hasSettings && settings.renameMovies.value;
const replaceIllegalCharacters =
hasSettings && settings.replaceIllegalCharacters.value;
const colonReplacementOptions = [
{ key: 'delete', value: translate('Delete') },
{ key: 'dash', value: translate('ReplaceWithDash') },
{ key: 'spaceDash', value: translate('ReplaceWithSpaceDash') },
{ key: 'spaceDashSpace', value: translate('ReplaceWithSpaceDashSpace') },
{
key: 'smart',
value: translate('SmartReplace'),
hint: translate('SmartReplaceHint'),
},
];
const standardMovieFormatHelpTexts = [];
const standardMovieFormatErrors = [];
const movieFolderFormatHelpTexts = [];
const movieFolderFormatErrors = [];
if (examplesPopulated) {
if (examples.movieExample) {
standardMovieFormatHelpTexts.push(
`${translate('Movie')}: ${examples.movieExample}`
);
} else {
standardMovieFormatErrors.push({
message: translate('MovieInvalidFormat'),
});
}
if (examples.movieFolderExample) {
movieFolderFormatHelpTexts.push(
`${translate('Example')}: ${examples.movieFolderExample}`
);
} else {
movieFolderFormatErrors.push({ message: translate('InvalidFormat') });
}
}
return (
<FieldSet legend={translate('MovieNaming')}>
{isFetching ? <LoadingIndicator /> : null}
{!isFetching && error ? (
<Alert kind={kinds.DANGER}>
{translate('NamingSettingsLoadError')}
</Alert>
) : null}
{hasSettings && !isFetching && !error ? (
<Form>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('RenameMovies')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="renameMovies"
helpText={translate('RenameMoviesHelpText')}
onChange={handleInputChange}
{...settings.renameMovies}
/>
</FormGroup>
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ReplaceIllegalCharacters')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="replaceIllegalCharacters"
helpText={translate('ReplaceIllegalCharactersHelpText')}
onChange={handleInputChange}
{...settings.replaceIllegalCharacters}
/>
</FormGroup>
{replaceIllegalCharacters ? (
<FormGroup size={sizes.MEDIUM}>
<FormLabel>{translate('ColonReplacement')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="colonReplacementFormat"
values={colonReplacementOptions}
helpText={translate('ColonReplacementFormatHelpText')}
onChange={handleInputChange}
{...settings.colonReplacementFormat}
/>
</FormGroup>
) : null}
{renameMovies ? (
<FormGroup size={sizes.LARGE}>
<FormLabel>{translate('StandardMovieFormat')}</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="standardMovieFormat"
buttons={
<FormInputButton onPress={onStandardNamingModalOpenClick}>
?
</FormInputButton>
}
onChange={handleInputChange}
{...settings.standardMovieFormat}
helpTexts={standardMovieFormatHelpTexts}
errors={[
...standardMovieFormatErrors,
...settings.standardMovieFormat.errors,
]}
/>
</FormGroup>
) : null}
<FormGroup
advancedSettings={advancedSettings}
isAdvanced={true}
size={sizes.MEDIUM}
>
<FormLabel>{translate('MovieFolderFormat')}</FormLabel>
<FormInputGroup
inputClassName={styles.namingInput}
type={inputTypes.TEXT}
name="movieFolderFormat"
buttons={
<FormInputButton onPress={onMovieFolderNamingModalOpenClick}>
?
</FormInputButton>
}
onChange={handleInputChange}
{...settings.movieFolderFormat}
helpTexts={[
translate('MovieFolderFormatHelpText'),
...movieFolderFormatHelpTexts,
]}
errors={[
...movieFolderFormatErrors,
...settings.movieFolderFormat.errors,
]}
/>
</FormGroup>
{namingModalOptions ? (
<NamingModal
isOpen={isNamingModalOpen}
{...namingModalOptions}
value={settings[namingModalOptions.name].value}
onInputChange={handleInputChange}
onModalClose={setNamingModalClosed}
/>
) : null}
</Form>
) : null}
</FieldSet>
);
}
export default Naming;
@@ -1,97 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import { fetchNamingExamples, fetchNamingSettings, setNamingSettingsValue } from 'Store/Actions/settingsActions';
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
import Naming from './Naming';
const SECTION = 'naming';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.advancedSettings,
(state) => state.settings.namingExamples,
createSettingsSectionSelector(SECTION),
(advancedSettings, examples, sectionSettings) => {
return {
advancedSettings,
examples: examples.item,
examplesPopulated: !_.isEmpty(examples.item),
...sectionSettings
};
}
);
}
const mapDispatchToProps = {
fetchNamingSettings,
setNamingSettingsValue,
fetchNamingExamples,
clearPendingChanges
};
class NamingConnector extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._namingExampleTimeout = null;
}
componentDidMount() {
this.props.fetchNamingSettings();
this.props.fetchNamingExamples();
}
componentWillUnmount() {
this.props.clearPendingChanges({ section: `settings.${SECTION}` });
}
//
// Control
_fetchNamingExamples = () => {
this.props.fetchNamingExamples();
};
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setNamingSettingsValue({ name, value });
if (this._namingExampleTimeout) {
clearTimeout(this._namingExampleTimeout);
}
this._namingExampleTimeout = setTimeout(this._fetchNamingExamples, 1000);
};
//
// Render
render() {
return (
<Naming
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
{...this.props}
/>
);
}
}
NamingConnector.propTypes = {
fetchNamingSettings: PropTypes.func.isRequired,
setNamingSettingsValue: PropTypes.func.isRequired,
fetchNamingExamples: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(NamingConnector);
@@ -1,506 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import Modal from 'Components/Modal/Modal';
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, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import NamingOption from './NamingOption';
import styles from './NamingModal.css';
const separatorOptions = [
{
key: ' ',
get value() {
return `${translate('Space')} ( )`;
}
},
{
key: '.',
get value() {
return `${translate('Period')} (.)`;
}
},
{
key: '_',
get value() {
return `${translate('Underscore')} (_)`;
}
},
{
key: '-',
get value() {
return `${translate('Dash')} (-)`;
}
}
];
const caseOptions = [
{
key: 'title',
get value() {
return translate('DefaultCase');
}
},
{
key: 'lower',
get value() {
return translate('Lowercase');
}
},
{
key: 'upper',
get value() {
return translate('Uppercase');
}
}
];
const fileNameTokens = [
{
token: '{Movie Title} - {Quality Full}',
example: 'Movie Title (2010) - HDTV-720p Proper'
}
];
const movieTokens = [
{ token: '{Movie Title}', example: 'Movie\'s Title', footNote: 1 },
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: 1 },
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: 1 },
{ token: '{Movie TitleThe}', example: 'Movie\'s Title, The', footNote: 1 },
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
{ token: '{Movie CleanOriginalTitle}', example: 'Τίτλος ταινίας', footNote: 1 },
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
{ token: '{Movie Collection}', example: 'The Movie Collection', footNote: 1 },
{ token: '{Movie Certification}', example: 'R' },
{ token: '{Release Year}', example: '2009' }
];
const movieIdTokens = [
{ token: '{ImdbId}', example: 'tt12345' },
{ token: '{TmdbId}', example: '123456' }
];
const qualityTokens = [
{ token: '{Quality Full}', example: 'HDTV-720p Proper' },
{ token: '{Quality Title}', example: 'HDTV-720p' }
];
const mediaInfoTokens = [
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: 1 },
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: 1 },
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: 1 },
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
{ token: '{MediaInfo VideoDynamicRange}', example: 'HDR' },
{ token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' },
{ token: '{MediaInfo 3D}', example: '3D' }
];
const releaseGroupTokens = [
{ token: '{Release Group}', example: 'Rls Grp', footNote: 1 }
];
const editionTokens = [
{ token: '{Edition Tags}', example: 'IMAX', footNote: 1 }
];
const customFormatTokens = [
{ token: '{Custom Formats}', example: 'Surround Sound x264' },
{ token: '{Custom Format:FormatName}', example: 'AMZN' }
];
const originalTokens = [
{ token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' },
{ token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' }
];
class NamingModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._selectionStart = null;
this._selectionEnd = null;
this.state = {
separator: ' ',
case: 'title'
};
}
//
// Listeners
onTokenSeparatorChange = (event) => {
this.setState({ separator: event.value });
};
onTokenCaseChange = (event) => {
this.setState({ case: event.value });
};
onInputSelectionChange = (selectionStart, selectionEnd) => {
this._selectionStart = selectionStart;
this._selectionEnd = selectionEnd;
};
onOptionPress = ({ isFullFilename, tokenValue }) => {
const {
name,
value,
onInputChange
} = this.props;
const selectionStart = this._selectionStart;
const selectionEnd = this._selectionEnd;
if (isFullFilename) {
onInputChange({ name, value: tokenValue });
} else if (selectionStart == null) {
onInputChange({
name,
value: `${value}${tokenValue}`
});
} else {
const start = value.substring(0, selectionStart);
const end = value.substring(selectionEnd);
const newValue = `${start}${tokenValue}${end}`;
onInputChange({ name, value: newValue });
this._selectionStart = newValue.length - 1;
this._selectionEnd = newValue.length - 1;
}
};
//
// Render
render() {
const {
name,
value,
isOpen,
advancedSettings,
additional,
onInputChange,
onModalClose
} = this.props;
const {
separator: tokenSeparator,
case: tokenCase
} = this.state;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('FileNameTokens')}
</ModalHeader>
<ModalBody>
<div className={styles.namingSelectContainer}>
<SelectInput
className={styles.namingSelect}
name="separator"
value={tokenSeparator}
values={separatorOptions}
onChange={this.onTokenSeparatorChange}
/>
<SelectInput
className={styles.namingSelect}
name="case"
value={tokenCase}
values={caseOptions}
onChange={this.onTokenCaseChange}
/>
</div>
{
!advancedSettings &&
<FieldSet legend={translate('FileNames')}>
<div className={styles.groups}>
{
fileNameTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
isFullFilename={true}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
size={sizes.LARGE}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
}
<FieldSet legend={translate('Movie')}>
<div className={styles.groups}>
{
movieTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('MovieFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('MovieID')}>
<div className={styles.groups}>
{
movieIdTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
{
additional &&
<div>
<FieldSet legend={translate('Quality')}>
<div className={styles.groups}>
{
qualityTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
<FieldSet legend={translate('MediaInfo')}>
<div className={styles.groups}>
{
mediaInfoTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('MediaInfoFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('ReleaseGroup')}>
<div className={styles.groups}>
{
releaseGroupTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('Edition')}>
<div className={styles.groups}>
{
editionTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('EditionFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('CustomFormats')}>
<div className={styles.groups}>
{
customFormatTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
<FieldSet legend={translate('Original')}>
<div className={styles.groups}>
{
originalTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
name={name}
value={value}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
size={sizes.LARGE}
onPress={this.onOptionPress}
/>
);
}
)
}
</div>
</FieldSet>
</div>
}
</ModalBody>
<ModalFooter>
<TextInput
name={name}
value={value}
onChange={onInputChange}
onSelectionChange={this.onInputSelectionChange}
/>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
NamingModal.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
isOpen: PropTypes.bool.isRequired,
advancedSettings: PropTypes.bool.isRequired,
additional: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
NamingModal.defaultProps = {
additional: false
};
export default NamingModal;
@@ -0,0 +1,469 @@
import React, { useCallback, useState } from 'react';
import FieldSet from 'Components/FieldSet';
import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import Modal from 'Components/Modal/Modal';
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, sizes } from 'Helpers/Props';
import NamingConfig from 'typings/Settings/NamingConfig';
import translate from 'Utilities/String/translate';
import NamingOption from './NamingOption';
import TokenCase from './TokenCase';
import TokenSeparator from './TokenSeparator';
import styles from './NamingModal.css';
const separatorOptions: { key: TokenSeparator; value: string }[] = [
{
key: ' ',
get value() {
return `${translate('Space')} ( )`;
},
},
{
key: '.',
get value() {
return `${translate('Period')} (.)`;
},
},
{
key: '_',
get value() {
return `${translate('Underscore')} (_)`;
},
},
{
key: '-',
get value() {
return `${translate('Dash')} (-)`;
},
},
];
const caseOptions: { key: TokenCase; value: string }[] = [
{
key: 'title',
get value() {
return translate('DefaultCase');
},
},
{
key: 'lower',
get value() {
return translate('Lowercase');
},
},
{
key: 'upper',
get value() {
return translate('Uppercase');
},
},
];
const fileNameTokens = [
{
token:
'{Movie Title} ({Release Year}) - {Edition Tags }{[Custom Formats]}{[Quality Full]}{-Release Group}',
example:
'The Movie - Title (2010) - Ultimate Extended Edition [Surround Sound x264][Bluray-1080p Proper]-EVOLVE',
},
{
token:
'{Movie CleanTitle} {Release Year} - {Edition Tags }{[Custom Formats]}{[Quality Full]}{-Release Group}',
example:
'The Movie Title 2010 - Ultimate Extended Edition [Surround Sound x264][Bluray-1080p Proper]-EVOLVE',
},
{
token:
'{Movie.CleanTitle}{.Release.Year}{.Edition.Tags}{.Custom.Formats}{.Quality.Full}{-Release Group}',
example:
'The.Movie.Title.2010.Ultimate.Extended.Edition.Surround.Sound.x264.Bluray-1080p.Proper-EVOLVE',
},
];
const movieTokens = [
{ token: '{Movie Title}', example: "Movie's Title", footNote: true },
{ token: '{Movie Title:DE}', example: 'Titel des Films', footNote: true },
{ token: '{Movie CleanTitle}', example: 'Movies Title', footNote: true },
{
token: '{Movie CleanTitle:DE}',
example: 'Titel des Films',
footNote: true,
},
{ token: '{Movie TitleThe}', example: "Movie's Title, The", footNote: true },
{
token: '{Movie CleanTitleThe}',
example: 'Movies Title, The',
footNote: true,
},
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας', footNote: true },
{
token: '{Movie CleanOriginalTitle}',
example: 'Τίτλος ταινίας',
footNote: true,
},
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
{ token: '{Movie TitleFirstCharacter:DE}', example: 'T' },
{
token: '{Movie Collection}',
example: 'The Movie Collection',
footNote: true,
},
{ token: '{Movie Certification}', example: 'R' },
{ token: '{Release Year}', example: '2009' },
];
const movieIdTokens = [
{ token: '{ImdbId}', example: 'tt12345' },
{ token: '{TmdbId}', example: '123456' },
];
const qualityTokens = [
{ token: '{Quality Full}', example: 'HDTV-720p Proper' },
{ token: '{Quality Title}', example: 'HDTV-720p' },
];
const mediaInfoTokens = [
{ token: '{MediaInfo Simple}', example: 'x264 DTS' },
{ token: '{MediaInfo Full}', example: 'x264 DTS [EN+DE]', footNote: true },
{ token: '{MediaInfo AudioCodec}', example: 'DTS' },
{ token: '{MediaInfo AudioChannels}', example: '5.1' },
{ token: '{MediaInfo AudioLanguages}', example: '[EN+DE]', footNote: true },
{ token: '{MediaInfo SubtitleLanguages}', example: '[DE]', footNote: true },
{ token: '{MediaInfo VideoCodec}', example: 'x264' },
{ token: '{MediaInfo VideoBitDepth}', example: '10' },
{ token: '{MediaInfo VideoDynamicRange}', example: 'HDR' },
{ token: '{MediaInfo VideoDynamicRangeType}', example: 'DV HDR10' },
{ token: '{MediaInfo 3D}', example: '3D' },
];
const releaseGroupTokens = [
{ token: '{Release Group}', example: 'Rls Grp', footNote: true },
];
const editionTokens = [
{ token: '{Edition Tags}', example: 'IMAX', footNote: true },
];
const customFormatTokens = [
{ token: '{Custom Formats}', example: 'Surround Sound x264' },
{ token: '{Custom Format:FormatName}', example: 'AMZN' },
];
const originalTokens = [
{ token: '{Original Title}', example: 'Movie.Title.HDTV.x264-EVOLVE' },
{ token: '{Original Filename}', example: 'movie title hdtv.x264-Evolve' },
];
interface NamingModalProps {
isOpen: boolean;
name: keyof Pick<NamingConfig, 'standardMovieFormat' | 'movieFolderFormat'>;
value: string;
movie?: boolean;
additional?: boolean;
onInputChange: ({ name, value }: { name: string; value: string }) => void;
onModalClose: () => void;
}
function NamingModal(props: NamingModalProps) {
const {
isOpen,
name,
value,
movie = false,
additional = false,
onInputChange,
onModalClose,
} = props;
const [tokenSeparator, setTokenSeparator] = useState<TokenSeparator>(' ');
const [tokenCase, setTokenCase] = useState<TokenCase>('title');
const [selectionStart, setSelectionStart] = useState<number | null>(null);
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
const handleTokenSeparatorChange = useCallback(
({ value }: { value: TokenSeparator }) => {
setTokenSeparator(value);
},
[setTokenSeparator]
);
const handleTokenCaseChange = useCallback(
({ value }: { value: TokenCase }) => {
setTokenCase(value);
},
[setTokenCase]
);
const handleInputSelectionChange = useCallback(
(selectionStart: number, selectionEnd: number) => {
setSelectionStart(selectionStart);
setSelectionEnd(selectionEnd);
},
[setSelectionStart, setSelectionEnd]
);
const handleOptionPress = useCallback(
({
isFullFilename,
tokenValue,
}: {
isFullFilename: boolean;
tokenValue: string;
}) => {
if (isFullFilename) {
onInputChange({ name, value: tokenValue });
} else if (selectionStart == null || selectionEnd == null) {
onInputChange({
name,
value: `${value}${tokenValue}`,
});
} else {
const start = value.substring(0, selectionStart);
const end = value.substring(selectionEnd);
const newValue = `${start}${tokenValue}${end}`;
onInputChange({ name, value: newValue });
setSelectionStart(newValue.length - 1);
setSelectionEnd(newValue.length - 1);
}
},
[name, value, selectionEnd, selectionStart, onInputChange]
);
return (
<Modal isOpen={isOpen} onModalClose={onModalClose}>
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{movie ? translate('FileNameTokens') : translate('FolderNameTokens')}
</ModalHeader>
<ModalBody>
<div className={styles.namingSelectContainer}>
<SelectInput
className={styles.namingSelect}
name="separator"
value={tokenSeparator}
values={separatorOptions}
onChange={handleTokenSeparatorChange}
/>
<SelectInput
className={styles.namingSelect}
name="case"
value={tokenCase}
values={caseOptions}
onChange={handleTokenCaseChange}
/>
</div>
{movie ? (
<FieldSet legend={translate('FileNames')}>
<div className={styles.groups}>
{fileNameTokens.map(({ token, example }) => (
<NamingOption
key={token}
token={token}
example={example}
isFullFilename={true}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
size={sizes.LARGE}
onPress={handleOptionPress}
/>
))}
</div>
</FieldSet>
) : null}
<FieldSet legend={translate('Movie')}>
<div className={styles.groups}>
{movieTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
/>
);
})}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('MovieFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('MovieID')}>
<div className={styles.groups}>
{movieIdTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
/>
);
})}
</div>
</FieldSet>
{additional ? (
<div>
<FieldSet legend={translate('Quality')}>
<div className={styles.groups}>
{qualityTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
/>
);
})}
</div>
</FieldSet>
<FieldSet legend={translate('MediaInfo')}>
<div className={styles.groups}>
{mediaInfoTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
/>
);
})}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('MediaInfoFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('ReleaseGroup')}>
<div className={styles.groups}>
{releaseGroupTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
/>
);
})}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('ReleaseGroupFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('Edition')}>
<div className={styles.groups}>
{editionTokens.map(({ token, example, footNote }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
footNote={footNote}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
/>
);
})}
</div>
<div className={styles.footNote}>
<Icon className={styles.icon} name={icons.FOOTNOTE} />
<InlineMarkdown data={translate('EditionFootNote')} />
</div>
</FieldSet>
<FieldSet legend={translate('CustomFormats')}>
<div className={styles.groups}>
{customFormatTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
onPress={handleOptionPress}
/>
);
})}
</div>
</FieldSet>
<FieldSet legend={translate('Original')}>
<div className={styles.groups}>
{originalTokens.map(({ token, example }) => {
return (
<NamingOption
key={token}
token={token}
example={example}
tokenSeparator={tokenSeparator}
tokenCase={tokenCase}
size={sizes.LARGE}
onPress={handleOptionPress}
/>
);
})}
</div>
</FieldSet>
</div>
) : null}
</ModalBody>
<ModalFooter>
<TextInput
name={name}
value={value}
onChange={onInputChange}
onSelectionChange={handleInputSelectionChange}
/>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default NamingModal;

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