1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-09 15:00:03 -04:00

Compare commits

...

198 Commits

Author SHA1 Message Date
Taloth Saldono
94f8e38d5a Implemented experimental Script Console for debugging with editor in the diag ui. 2020-06-07 00:14:08 +02:00
Taloth Saldono
031371652b Used ReflectionOnly and/or public types where possible to avoid loading related assemblies unnecessarily. 2020-06-06 23:12:31 +02:00
Taloth Saldono
02104aff34 jsconfig for a bit of autocompletion and intellisense 2020-06-06 23:12:31 +02:00
Taloth Saldono
5bd1c47ca7 Revised webpack bundling and updated worker loading, turned inline worker on by default. 2020-06-06 23:12:31 +02:00
Taloth Saldono
f846e0c031 Fixed flaky test. 2020-06-06 22:28:08 +02:00
Taloth Saldono
72b0f640f4 Added Plex url to cleanser 2020-06-06 22:28:08 +02:00
Mark McDowall
430af0401c New: Use release quality source if not in downloaded file and resolution matches 2020-06-06 10:47:00 -07:00
Mark McDowall
0ff08dbe8d Fixed: Error when processing release with files Sonarr is unable to parse 2020-06-06 10:19:59 -07:00
Taloth Saldono
57cca9fcdc Fixed typo in Cleanse IP 2020-06-03 18:33:00 +02:00
Taloth Saldono
b3daa280c5 Cleanse remote IP Address from trace log file 2020-06-02 20:58:13 +02:00
Taloth Saldono
449c1caf55 Fixed: Mono not validating cross-signed certficates properly 2020-06-02 20:57:20 +02:00
Taloth Saldono
0c05236bee Support for Runtime Patches via Harmony 2020-06-02 20:57:20 +02:00
Mark McDowall
9f54ff8169 Fixed: Interactive search for anime season even if all episodes are unmonitored
Fixes #3791
2020-06-01 21:33:53 -07:00
Taloth Saldono
f2e1b4e435 Log contents on api errors during tests. 2020-06-01 15:01:40 +02:00
Taloth Saldono
1e80361c3a Fixed tests and missing logger initialization 2020-06-01 14:01:09 +02:00
Taloth Saldono
3564be19a8 Fixed typo 2020-06-01 00:26:09 +02:00
Mark McDowall
14c9b6aaf4 Additional logging when trying to complete tracked downloads 2020-05-30 16:40:41 -07:00
Mark McDowall
069fc5cd33 Mass Editor size and options
New: Option to show size in Mass Editor
New: Size on Disk in Mass Editor custom filters

Closes #3273
2020-05-30 13:56:28 -07:00
Mark McDowall
3586d7042b Fixed: Auto-focusing Filter series import during import series 2020-05-30 11:04:13 -07:00
Mark McDowall
a32b6276bd Fixed: Deleting row from middle of filter builder leading to error 2020-05-30 10:46:42 -07:00
Mark McDowall
b0e31629b5 Fixed: Not removing seeded download if it was manual imported in some cases 2020-05-30 10:18:17 -07:00
Mark McDowall
e2644c3847 Fixed manual import possible null series 2020-05-30 10:12:24 -07:00
Mark McDowall
7ea45bb714 Fix some styling issues in Quality Profile and Release Profiles 2020-05-30 10:11:34 -07:00
Mark McDowall
f524fcd3e4 Fixed: Skip missing episode title check if file is already in series folder 2020-05-30 10:09:46 -07:00
Mark McDowall
6c418302f8 Fixed: Episode file renamed event stored language properly
Closes #3783
2020-05-27 23:52:37 -07:00
Mark McDowall
b28d329654 Fixed: Size on disk with seasons over 100
Fixes #3774
2020-05-24 23:39:57 -07:00
Mark McDowall
ebe2ad1520 New: Show size on disk for each season
Closes #3432
2020-05-24 14:38:37 -07:00
Mark McDowall
41dfb677e7 Fixed: Rejections custom filter for Interactive Search (now Rejections Count) 2020-05-24 14:02:30 -07:00
Mark McDowall
c646bef369 Calendar item/episode status fixes
New: Calendar shows icon when download is complete and not yet imported
New: Episode status shows pending import and importing icon
2020-05-24 13:32:33 -07:00
Mark McDowall
910de6d94a Queue status/timeleft improvements
New: Queue status icon is purple when download is waiting to import or importing
Fixed: Timeleft on Queue won't show when completed
Closes #3743
2020-05-24 13:30:33 -07:00
Mark McDowall
5951992bd5 Fixed: Preferred words remove button in Firefox
Fixes #3710
2020-05-24 12:06:35 -07:00
Mark McDowall
cb9d78064a Fixed: Width of episode column with warning
Closes #3733
2020-05-24 11:39:50 -07:00
Mark McDowall
fd608fd411 New: Don't close manual import when clicking outside the modal
Closes #3761
2020-05-24 10:38:30 -07:00
Mark McDowall
d3bd90e4b9 Fixed: Manual import for unknown series items will properly mark as imported 2020-05-24 10:11:10 -07:00
Mark McDowall
4988655568 Store language with deleted episode history 2020-05-24 10:10:24 -07:00
ildoc
098db08ede updated readme 2020-05-21 10:58:35 +02:00
Skyler Mäntysaari
93e3e92bba New: SendGrid Notifications
Closes #3341
2020-05-20 11:22:05 -07:00
Taloth Saldono
bdfdd28d6a Fixed: Added .org to website url filtering in parser 2020-05-19 23:01:21 +02:00
Taloth Saldono
a75e10c4c9 Fixed: Parsing anime dual language titles
closes #3756
2020-05-18 01:33:03 +02:00
Taloth Saldono
5251db7224 Fixed recursion issue when emptying recycle bin 2020-05-16 19:22:47 +02:00
Taloth Saldono
4d1a4d4241 Updated kodi url 2020-05-13 21:29:21 +02:00
Taloth Saldono
d3a22459ac Fixed: Performance issue when scanning large root folder 2020-05-13 21:27:39 +02:00
Qstick
4f7e00bdc4 Fixed: Don't lock command queue if updating is disabled 2020-05-13 19:33:14 +02:00
Mark McDowall
1199ae4e4f New: Use filename for preferred word score if it's higher than scene name 2020-05-09 16:45:42 -07:00
Mark McDowall
36088ef49d Fixed: Tag details list series in alphabetical order 2020-05-09 16:45:42 -07:00
Taloth Saldono
7ffb2eb440 Replaced matchAll usage since it's not available on all browsers 2020-05-06 14:33:14 +02:00
Taloth Saldono
0716d0931a Added UserAgent to api request trace log 2020-05-05 20:14:07 +02:00
Taloth Saldono
66ee28d0a9 Lock CommandQueueManager.PushMany too 2020-05-03 18:48:20 +02:00
Taloth Saldono
1487f54749 Skip unknown/removed commands still queued in the database 2020-05-03 17:29:46 +02:00
Taloth Saldono
013c46d266 Fixed timing issue allowing multiple instances of the same command to be queued 2020-05-03 17:29:46 +02:00
Taloth Saldono
c8d2fcb223 Added UpdateMechanismMessage to allow package maintainers provide custom message 2020-05-03 17:29:46 +02:00
Taloth Saldono
5288b61378 Inline markdown-style link for PackageAuthor 2020-05-03 17:28:38 +02:00
Taloth Saldono
a2679f64ee Parse WEB at the end of release title. 2020-05-03 17:00:11 +02:00
Mark McDowall
5d9dfee3c0 New: Add DownloadClient and DownloadId to Webhook notifications 2020-05-02 21:02:11 -07:00
Mark McDowall
98f9323b42 Fixed: Root folder custom filter in Mass Editor 2020-05-02 21:02:11 -07:00
Taloth Saldono
f282ae8aae Prevent exception parsing unicode digits in absolute numbers. 2020-05-02 14:21:58 +02:00
Mark McDowall
0b1e99991e Fixed: Imports triggered through API not being marked as imported/removed from client
Fixes #3717
2020-04-29 00:08:01 -07:00
Mark McDowall
75be036a87 Fixed: Imported downloads not being removed when seeding goals are met
Fixes #3693
2020-04-28 21:37:26 -07:00
Taloth Saldono
23dc7794f1 Fixed: Generating Kodi episode metadata files when scanning series folder 2020-04-28 23:34:52 +02:00
Taloth Saldono
b8e2f3d716 Clarify that Post-Import Category torrents are not monitored by Sonarr.
Configure Deluge to remove such torrents when seeding criteria has been met.

closes #3659
2020-04-28 22:10:30 +02:00
Taloth Saldono
686a14cdff Log Real IP on Authentication failure in case of a reverse proxy
closes #3711
2020-04-27 23:58:35 +02:00
Taloth Saldono
b4405b0600 Fixed: Parsing release group from file rather than folder in case of season packs 2020-04-27 19:09:49 +02:00
Qstick
d4bcf28d08 Add missing "does" to DifferentQualitySpec message 2020-04-26 13:28:38 -07:00
rg9400
4bacc35605 Fixed: Indicate unchecking Replace Illegal Characters will remove them 2020-04-26 12:54:18 -07:00
Mark McDowall
417340c2c6 Fixed: Manual imports of multi-episode files being treated as fully imported 2020-04-25 14:13:29 -07:00
Mark McDowall
79d8a9d44b Fixed: Episodes removed from queue re-appearing on refresh
Fixes #3697
2020-04-25 14:12:38 -07:00
Mark McDowall
68440bba4d Fixed: Rejection message for quality mismatch 2020-04-25 14:09:57 -07:00
Mark McDowall
0719c83da4 Fixed: Parsing of some anime batch releases
Fixes #3698
2020-04-25 13:33:30 -07:00
Mark McDowall
5a7dec34cc Fixed: Rotating mobile device when modal is open won't reset modal
Closes #3333
2020-04-25 11:59:29 -07:00
Mark McDowall
9d766cfed5 Fixed: Remove seeded downloads if they've finished seeding after import
Fixes #3693
2020-04-25 11:49:18 -07:00
Mark McDowall
1498f4e361 Revert: Prevent an edge case where a download is not marked as complete 2020-04-21 10:27:27 -07:00
Taloth Saldono
05dd17aacb Added support for title query parameter to newznab/torznab, receiving raw series title 2020-04-21 18:44:08 +02:00
Taloth Saldono
200aee52f7 New: Searching for episodes with season level scene mapping now possible instead of only via RssSync (Newznab/Torznab only) 2020-04-21 18:44:08 +02:00
Mark McDowall
d6dd13a6be Prevent an edge case where a download is not marked as complete 2020-04-20 18:09:24 -07:00
Mark McDowall
be3b3df903 Don't reject for having the same file size
Fixed: Remove same file size rejection during import
Fixed: Reject imports for non-season pack files if quality of file doesn't match grabbed quality
Closes #3691
2020-04-20 17:58:59 -07:00
Mark McDowall
f2a56b29d9 Fixed: Windows installer won't create shortcut if unchecked 2020-04-20 17:57:09 -07:00
Mark McDowall
27d98868b8 Fixed: Can ignore queue items with unknown episodes 2020-04-20 17:56:36 -07:00
Mark McDowall
7f28ab895a Small change to creating an itemMap during item update 2020-04-20 17:56:22 -07:00
Mark McDowall
97ec184754 Fixed: Import series failing to add items to process 2020-04-20 09:35:35 -07:00
Mark McDowall
42343d5283 Add class to allow for overriding scrollbar width 2020-04-18 20:21:46 -07:00
Mark McDowall
479baf06a7 Fixed: Removed items in queue still showing until refresh 2020-04-18 20:21:29 -07:00
Mark McDowall
7f7d196e44 Fixed: Don't process downloads removed from the client
Fixes #3557
2020-04-18 20:21:29 -07:00
Mark McDowall
c862fd9ff6 Don't re-trigger completed event 2020-04-18 20:21:29 -07:00
Mark McDowall
770b89c2b3 Track fully imported downloads in separate history table
New: Improved detection of already imported downloads
Closes #3554
2020-04-18 20:21:29 -07:00
Taloth Saldono
576275b6da Another mono 6.x workaround to use rename rather than expensive copy 2020-04-18 11:08:47 +02:00
Taloth Saldono
776191b3bd Improved error message when nzb download contains an newznab error instead 2020-04-17 00:14:05 +02:00
Mark McDowall
d369d85699 Fixed: Ended overlay on series posters 2020-04-15 09:14:05 -07:00
Mark McDowall
552fac0466 More strict ExcludedSubFoldersRegex 2020-04-15 09:13:26 -07:00
Mark McDowall
a348d98dd9 Fixed: Filter direct excluded subfolders of the selected directory during manual import 2020-04-13 21:30:40 -07:00
Mark McDowall
ccdfdd1049 Fix checkingUP qbit status unit test 2020-04-12 12:31:17 -07:00
Mark McDowall
f0ca636654 Fixed sort in HistoryRepository 2020-04-09 22:58:42 -07:00
Mark McDowall
b5e734b9e5 Fixed: Ignore .@__thumb folders 2020-04-09 22:58:24 -07:00
Mark McDowall
e1639d35a2 Fixed: Series toolbar button collapsing 2020-04-09 22:58:08 -07:00
Mark McDowall
9b99ad27cd Fixed: Tooltip for existing series on add new series item 2020-04-09 22:57:39 -07:00
Mark McDowall
bba57bb434 Fixed: Queue not always clearing checked items when updated 2020-04-03 08:44:58 -07:00
Mark McDowall
8c24cd9864 Fixed: Strip AlteZachen from release group name 2020-04-02 17:27:16 -07:00
Mark McDowall
91de7ff11c Fixed: Don't try to render quality when it's null
Fixes #3649
2020-04-02 17:24:18 -07:00
Mark McDowall
9702d2e5ad Fixed: Treated checkingUP status from Qbit as queued in case it fails to validate 2020-04-02 17:23:25 -07:00
Anthony Borushko
638066db03 Fixed: Tag inputs respect non-QWERTY layouts 2020-03-31 09:57:27 -07:00
Jef LeCompte
1b3839ac0d Updated README 2020-03-31 09:21:52 -07:00
Mark McDowall
219494ea9d Fixed: Preferred word can't have a term that is empty or only spaces 2020-03-29 14:54:14 -07:00
Mark McDowall
642f75761f GetBestRootFolderPathFixture OS Agnostic paths 2020-03-28 12:43:05 -07:00
Mark McDowall
ed28f94f02 Improve root folder health check 2020-03-27 15:24:20 -07:00
Mark McDowall
618c611a59 Fixed: Series Network filter breaking if network was not available 2020-03-22 22:50:18 -07:00
Mark McDowall
00821b7ad6 New: Parse multi-part episodes using date
Closes #381
2020-03-22 22:44:14 -07:00
Mark McDowall
84b9488cfb Fix broken test 2020-03-22 10:45:54 -07:00
Taloth Saldono
37ad801065 Fixed: Audio Channel Information missing in MediaInfo for certain mkv files with DTS audio 2020-03-22 12:02:51 +01:00
Taloth Saldono
4219cdb364 Fixed: RemotePoster on v3 api provides local url rather than thetvdb url 2020-03-22 12:02:51 +01:00
Mark McDowall
e23a879669 Fixed: Cutoff unmet searches rejecting releases incorrectly 2020-03-20 17:34:18 -07:00
Mark McDowall
4ddf4a22a3 Fixed: Enter on Delete profile confirmation deleting all unused profiles 2020-03-20 08:37:35 -07:00
Mark McDowall
72afb28c30 Revert failing parsing tests 2020-03-19 11:11:37 -07:00
Mark McDowall
eb51a42f60 Fixed: Sorting queue by episode properties when not all items have an episode 2020-03-19 10:11:07 -07:00
Mark McDowall
bc01384cc7 Actually fixed error rending queue row when quality is missing 2020-03-19 10:10:36 -07:00
Mark McDowall
00c922875f Fixed: Multiple series found during manual import prevents manual importing from folder
Fixes #3512
2020-03-18 19:30:02 -07:00
Mark McDowall
8c93d73b42 Fixed: Error rending queue row when quality is missing
Fixes #3614
2020-03-18 19:09:07 -07:00
Mark McDowall
3b6d60e904 New: RSS Sync button on Calendar
Closes #3326
2020-03-18 19:08:58 -07:00
Mark McDowall
a965b8e7b2 New: Filter episodes in API v3 by episode file ID
Closes #3589
2020-03-18 19:08:51 -07:00
Taloth Saldono
25abf52b3f Added update check early in startup if the package requested a post-install update check 2020-03-16 19:18:41 +01:00
Taloth Saldono
19764014be Increased mono dependency from 5.4 to 5.18 for debian
# Conflicts:
#	docker/tests/run-all.sh
2020-03-16 19:18:41 +01:00
Taloth Saldono
c91a5c80d3 Added .NET Framework 4.7.2 requirement check to windows installer 2020-03-16 19:18:41 +01:00
Taloth Saldono
e7b88c313d Fixed: Workaround for mono 6.x file copy/move issues 2020-03-16 19:18:41 +01:00
Taloth Saldono
9ac0864b61 Fixed scrolling issue in Root Path selector dropdown on mobile 2020-03-14 22:08:51 +01:00
Taloth Saldono
fcdd0f21c7 Fixed: Wrongly parsing language in series title for season packs (episodes were already handled) 2020-03-13 20:18:37 +01:00
Taloth Saldono
5497b68a98 Fixed: Don't auto-search newly added episodes on tvdb that aired more than 2 weeks ago
Fixed: Don't monitor newly added old episodes on tvdb if series was previously empty
2020-03-13 00:33:35 +01:00
Mark McDowall
50886ac928 More webook series properties
New: IMDB and TvMaze IDs in Webhooks
New: Series type in Webhooks
2020-03-10 23:58:34 -07:00
Mark McDowall
e2ff089232 Fixed: Metadata files not being created after rescan 2020-03-10 23:57:41 -07:00
Mark McDowall
ae7f8926f8 New: Ignore #recycle folders (Synology Recycle bin folder) 2020-03-10 23:56:09 -07:00
Mark McDowall
0bbc4e8c1b Fixed: Remove website post fix before parsing 2020-03-08 11:14:21 -07:00
Mark McDowall
295fdad750 Fixed: Broken tasks getting stuck in queue 2020-03-05 17:57:20 -08:00
Mark McDowall
63e01aff8c Fixed: Not importing upgrade for preferred language
Fixes #3605
2020-03-05 17:57:20 -08:00
unknown
e05ceb226c Update help text in Connections from Download to Import 2020-03-05 09:14:46 -08:00
Mark McDowall
1c699841c1 Fixed: Handle qBit ForcedDL State
Closes #3604
2020-03-05 09:13:44 -08:00
Mark McDowall
385c7fb0ce Fixed: Error occurred while executing task ProcessMonitoredDownloads 2020-03-03 18:10:29 -08:00
Mark McDowall
15d84046db Fixed: Inaccessible path leading to import process being aborted before processing all items
Fixes #3598
2020-03-03 16:54:12 -08:00
Mark McDowall
3ad396a9c2 Fixed: Re-add background to apple-touch-icon
This reverts commit afcfaace19.
2020-03-03 08:58:53 -08:00
Mark McDowall
77f886ceef OverlayScroller still needs to be used in PageContentBody 2020-03-02 14:06:36 -08:00
Taloth Saldono
8adb788205 Linting 2020-03-02 22:49:46 +01:00
Taloth Saldono
d731317c81 Fixed comment typo in webpack config 2020-03-02 22:48:44 +01:00
Mark McDowall
a824ce691b Fixed: Preferred is not an indexer field
Fixes #3595
2020-03-02 08:29:46 -08:00
Mark McDowall
506023b0f3 Scrolling and hotkey improvements
New: Use Esc/Enter for cancel/accept in confirmation modals
Fixed: Modals focused when opened
Fixed: Scrolling with keyboard unless focus is shifted out of scrollable area
Closes #3291
2020-03-01 21:03:59 -08:00
Taloth Saldono
52e5d4d0f1 Linting error 2020-03-01 22:26:49 +01:00
Taloth Saldono
00edffc0f4 Fixed random typo 2020-03-01 22:16:00 +01:00
Taloth Saldono
92f1f3e73a New: Added mediainfo formatting for E-AC3 Atmos 2020-03-01 22:16:00 +01:00
Taloth Saldono
1d339ad4f1 Belated removal of bitmetv and cleanup of usenet-crawler. 2020-03-01 22:16:00 +01:00
Jacob
99728a604d New: Added option to filter Release Profile to a specific indexer 2020-03-01 22:15:59 +01:00
netpok
c07a67ae3c New: Added aired-before field to kodi metadata to sort specials
closes #3073
2020-03-01 22:15:58 +01:00
Mark McDowall
be11789a86 New: Clone indexer button
Closes #3546
2020-03-01 12:56:58 -08:00
Mark McDowall
b8ce274fa5 Manual Import Sorting
Fixed: Manual Import sorting by quality
New: Manual Import sort by size
Closes #3334
2020-03-01 11:51:27 -08:00
Mark McDowall
d7967e3e1b Fix hasDifferentItems 2020-02-28 11:15:01 -08:00
ta264
746da69070 Fixed: UI slowdowns while tasks are running
Fixes #3480
2020-02-26 17:57:21 -08:00
ta264
b05b7ec4ad Trigger fewer signalr broadcasts 2020-02-26 17:57:21 -08:00
ta264
9abdaca079 New: Faster processing of special releases 2020-02-26 17:57:21 -08:00
ta264
5a79b8502e New: Improved Series list performance 2020-02-26 17:57:21 -08:00
ta264
466d4fba9e Don't rerender all cells each scroll 2020-02-26 17:57:21 -08:00
ta264
108f6fe393 Better selection of jump bar items
Show first, last and most common items
2020-02-26 17:57:21 -08:00
ta264
792896c46b New: Faster searching of existing series 2020-02-26 17:57:21 -08:00
ta264
43d04cd54e Faster series selector 2020-02-26 17:57:21 -08:00
ta264
283f905d79 Don't mutate state when sorting items 2020-02-26 17:57:21 -08:00
ta264
dd8d1b673e Faster hasDifferentItems and specialized OrOrder version 2020-02-26 17:57:21 -08:00
ta264
9ef64660ce Option for production build with profiling 2020-02-26 17:57:21 -08:00
Mark McDowall
88b1c8fc3e Fixed: Moving series folders in subfolders of the root folder when destination subfolder was missing 2020-02-26 17:45:13 -08:00
Mark McDowall
bcc8b655f7 Fixed: Re-processing imported download causing task to fail
Fixes #3501
2020-02-19 19:09:55 -08:00
Mark McDowall
438d9eb717 Fixed: Prompt to restart after resetting API key
Fixes #3580
2020-02-19 18:18:47 -08:00
Mark McDowall
2c0a0175ef Fixed: Sorting by episode count
Fixes #3531
2020-02-19 18:03:58 -08:00
Mark McDowall
e51f1b5e16 Fixed: Parsing of 360p releases
Fixes #3519
2020-02-19 17:38:17 -08:00
Mark McDowall
544108df37 Fixed: Import series when no results are returned from for a folder 2020-02-19 17:21:55 -08:00
beyondmeat
a23639e62e Fixed: Empty list message for System: Events 2020-02-19 17:19:18 -08:00
Taloth Saldono
cde5a6d1a4 Fixed stylelint errors 2020-02-11 21:41:16 +01:00
Taloth Saldono
b601c8bcfe New: Added advanced subtitle/audio language filter to {MediaInfo ..}
closes #3367
2020-02-11 21:13:13 +01:00
Taloth Saldono
023c8260f2 Added Norwegian Bokmal alias 2020-02-11 20:14:10 +01:00
Taloth Saldono
51e2e084af Added try-catch for DateTime.TryParse edgecase
closes #3518
2020-02-09 17:05:45 +01:00
Taloth Saldono
fc5dd8137f Support for VS2019 build environment 2020-02-07 21:16:53 +01:00
Taloth Saldono
268fc46ef7 Fixed: Representation of episode start time when not starting at the full hour in am/pm notation 2020-02-01 22:50:16 +01:00
Mark McDowall
010c65af9c Fixed: Don't monitor new seasons if series is not monitored
Fixes #3547
2020-02-01 13:03:11 -08:00
Mark McDowall
db42256dc3 Improve default series type handling (for daily series)
New: Display default series type when adding new/existing series when available
Fixed: Don't override series type on series refresh
2020-01-31 17:51:30 -08:00
Mark McDowall
e9b537b6e6 Fixed: Rejecting import for a release that was grabbed again 2020-01-31 17:51:30 -08:00
Mark McDowall
c615ef476a Fixed: Typo in unmonitored series tooltip
Fixes #3538
2020-01-31 17:51:30 -08:00
Mark McDowall
b93e8da235 Fixed: Force grabbing selected delayed items in queue 2020-01-31 17:51:30 -08:00
Pika
74a0a57468 BTN: Fix name 2020-01-19 18:40:06 +01:00
Петр Шургалин
b19d665817 Fixed: RestClient does not use global proxy settings 2020-01-19 16:41:31 +01:00
Taloth Saldono
10dc884fa8 Fixed: Posters not always showing when searching for new shows 2020-01-12 22:27:56 +01:00
Taloth Saldono
d8446c2d5a New: Added tvdb Upcoming series status 2020-01-12 22:27:55 +01:00
Mark McDowall
d3cd46bb51 New: Limit recent folders in Manual import to 10 and descending order
Closes #3491
2020-01-07 17:36:57 -08:00
Mark McDowall
bc0da03caf Fix proptype warning for id of EnhancedSelectInputOption 2020-01-07 17:11:45 -08:00
Mark McDowall
c0a356261b New: Added help text for qualities in groups
Closes #3495
2020-01-07 17:00:12 -08:00
Mark McDowall
fa4060b7fe Fixed: Previously imported downloads reappear in queue
Fixes #3496
2020-01-07 16:55:13 -08:00
Taloth Saldono
29117fc222 Fixed missing interface for the CheckForFinishedDownloadCommand backward compat handling
fixes #3492
2020-01-05 14:37:52 +01:00
julakali
24ba5e5bda Use msbuild instead of the deprecated xbuild 2020-01-04 17:54:25 -08:00
gl3nni3
2d94857369 Fixed: Replace duplicate slashes from file names when importing
Fixes #3470
2020-01-04 17:52:45 -08:00
Mark McDowall
c6ea7d7e63 Option to ignore items when removing from queue instead of removing from client
New: Option to not remove item from download client when removing from queue

Closes #1710
2020-01-04 17:49:39 -08:00
Mark McDowall
3916495329 Monitor and Process downloads separately
New: Queue remains up to date while importing file from remote file system
Fixed: Failed downloads still in queue won't result in failed search

Closes #668
Closes #907
Fixes #2973
2020-01-04 17:49:39 -08:00
Mark McDowall
4e965e59a9 Fixed: Parsing of Extended Multi-episode format file names 2019-12-30 09:27:24 -08:00
Taloth Saldono
0acb3aa32b Fixed: Regression in Multi-Episode format parser in previous release
fixes #3481
2019-12-30 13:06:25 +01:00
Mark McDowall
9189d8bf4d Fixed: Parsing of poorly named double episode releases
Fixes #3439
2019-12-29 02:32:30 -08:00
Mark McDowall
ec0c96bde4 Remove website prefixes with dashes in URL 2019-12-29 02:32:30 -08:00
Mark McDowall
562c8c4afe Fixed: Improved quality parsing from truncated release names
Closes #3345
2019-12-29 02:32:30 -08:00
Mark McDowall
fd6d4493c4 Fixed: Details for episode history flashing on mobile devices 2019-12-29 02:32:30 -08:00
Jayden
1a2419e096 Fix typo in remove queue item modal 2019-12-29 01:49:49 -08:00
557 changed files with 10508 additions and 3376 deletions

View File

@@ -1,58 +1,73 @@
# Sonarr
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
## Major Features Include:
## Getting Started
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically detects new episodes
* Can scan your existing library and download any missing episodes
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Fully configurable episode renaming
* Full integration with SABnzbd and NZBGet
* Full integration with Kodi, Plex (notification, library update, metadata)
* Full support for specials and multi-episode releases
* And a beautiful UI
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
## Support
- [Donate](https://sonarr.tv/donate)
- [Discord](https://discord.gg/M6BvZn5)
- [Reddit](https://www.reddit.com/r/sonarr)
## Features
### Current Features
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
- Automatically detects new episodes
- Can scan your existing library and download any missing episodes
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
- Automatic failed download handling will try another release if one fails
- Manual search so you can pick any release or to see why a release was not downloaded automatically
- Fully configurable episode renaming
- Full integration with SABnzbd and NZBGet
- Full integration with Kodi, Plex (notification, library update, metadata)
- Full support for specials and multi-episode releases
- And a beautiful UI
## Configuring Development Environment:
### Requirements
* [Visual Studio 2017](https://www.visualstudio.com/vs/)
* [Git](https://git-scm.com/downloads)
* [NodeJS](https://nodejs.org/en/download/)
* [Yarn](https://yarnpkg.com/)
- [Visual Studio 2017](https://www.visualstudio.com/vs)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download)
- [Yarn](https://yarnpkg.com)
### Setup
* Make sure all the required software mentioned above are installed
* Clone the repository into your development machine. [*info*](https://help.github.com/en/articles/working-with-forks)
* Grab the submodules `git submodule init && git submodule update`
* Install the required Node Packages `yarn`
- Make sure all the required software mentioned above are installed
- Clone the repository recursively to get Sonarr and it's submodules
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
- Install the required Node Packages using `yarn`
### Backend Development
* Run `yarn build` to build the UI
* Open `Sonarr.sln` in Visual Studio
* Make sure `NzbDrone.Console` is set as the startup project
* Build `NzbDrone.Windows` and `NzbDrone.Mono` projects
* Build Solution
- Run `yarn build` to build the UI
- Open `Sonarr.sln` in Visual Studio
- Make sure `Sonarr.Console` is set as the startup project
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
- Build Solution
### UI Development
* Run `yarn watch` to build UI and rebuild automatically when changes are detected
* Run Sonarr.Console.exe (or debug in Visual Studio)
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
- Run Sonarr.Console.exe (or debug in Visual Studio)
### License
### Licenses
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2019
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2020
### Sponsors
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
* [ReSharper](http://www.jetbrains.com/resharper/)
* [TeamCity](http://www.jetbrains.com/teamcity/)
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
- [ReSharper](http://www.jetbrains.com/resharper/)
- [TeamCity](http://www.jetbrains.com/teamcity/)

View File

@@ -88,13 +88,14 @@ CleanFolder()
BuildWithMSBuild()
{
installationPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -property installationPath`
installationPath=${installationPath/C:\\/\/c\/}
installationPath=${installationPath//\\/\/}
msBuild="$installationPath/MSBuild/$msBuildVersion/Bin"
echo $msBuild
msBuildPath=`$vswhere -latest -products \* -requires Microsoft.Component.MSBuild -find MSBuild\\\\\*\*\\\\Bin\\\\MSBuild.exe`
msBuildPath=${msBuildPath/C:\\/\/c\/}
msBuildPath=${msBuildPath//\\/\/}
msBuildDir=$(dirname "$msBuildPath")
export PATH=$msBuild:$PATH
echo $msBuildDir
export PATH=$msBuildDir:$PATH
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Clean //m
$nuget restore $slnFile
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
@@ -103,9 +104,9 @@ BuildWithMSBuild()
BuildWithXbuild()
{
export MONO_IOMAP=case
CheckExitCode xbuild /t:Clean $slnFile
CheckExitCode msbuild /t:Clean $slnFile
mono $nuget restore $slnFile
CheckExitCode xbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
CheckExitCode msbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
}
LintUI()

View File

@@ -7,8 +7,8 @@ Vcs-Git: git@github.com:Sonarr/Sonarr.git
Vcs-Browser: https://github.com/Sonarr/Sonarr
Build-Depends: debhelper (>= 9),
dh-systemd (>= 1.5),
mono-devel (>= 5.4),
libmono-cil-dev (>= 5.4),
mono-devel (>= 5.18),
libmono-cil-dev (>= 5.18),
cli-common-dev (>= 0.9+xamarin5)
Package: sonarr
@@ -16,7 +16,7 @@ Architecture: all
Provides: nzbdrone
Conflicts: nzbdrone
Replaces: nzbdrone
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.4), ca-certificates-mono, libmono-system-net-http4.0-cil (>= 4.0.0~alpha1), ${cli:Depends}, ${misc:Depends}
Depends: adduser, libsqlite3-0 (>= 3.7), libmediainfo0v5 (>= 0.7.52) | libmediainfo0 (>= 0.7.52), mono-runtime (>= 5.18), ca-certificates-mono, libmono-system-net-http4.0-cil (>= 4.0.0~alpha1), ${cli:Depends}, ${misc:Depends}
Recommends: libmediainfo0v5 (>= 18.03) | libmediainfo0 (>= 18.03)
Suggests: sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
Description: Internet PVR

View File

@@ -95,7 +95,7 @@ chown -R $USER:$GROUP /usr/lib/sonarr
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
#BEGIN BUILTIN UPDATER
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ]; then
if [ "$UPDATER" = "BuiltIn" ]; then
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
touch $CONFDIR/update_required
chown $USER:$GROUP $CONFDIR/update_required

View File

@@ -3,7 +3,7 @@
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
EXCLUDE_MODULEREFS = crypt32 httpapi
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
%:
dh $@ --with=systemd --with=cli

View File

@@ -1,7 +1,7 @@
FROM ubuntu:xenial AS builder
ENV DEBIAN_FRONTEND noninteractive
ENV MONO_VERSION 5.14
ENV MONO_VERSION 5.18
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \

View File

@@ -16,6 +16,7 @@ RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E03280
RUN apt-get update && apt-get install -y \
libmono-system-runtime4.0-cil \
libmono-system-net-http4.0-cil \
&& rm -rf /var/lib/apt/lists/*
COPY startup.sh /startup.sh

View File

@@ -2,16 +2,18 @@
opt_parallel=
opt_version=
opt_mode=both
while getopts 'pv:m:?h' c
while getopts 'pv:m:r?h' c
do
case $c in
p) opt_parallel=1 ;;
v) opt_version=$OPTARG ;;
m) opt_mode=$OPTARG ;;
r) opt_report=1 ;;
?|h) printf "Usage: %s [-p] [-v mono-ver] [-m sonarr|complete]\n" $0
printf " -p run parallel\n"
printf " -v run specified mono version\n"
printf " -m run only mono-'complete' or 'sonarr' package variants\n"
printf " -r only report\n"
exit 2
esac
done
@@ -20,17 +22,22 @@ done
# make sure that the docker host has enough memory to handle about ~300 MB per container, so 2-3 GB total
# excess goes to the swap and will slow down the entire system
# Preferred versions
MONO_VERSIONS="6.0 5.20 5.18"
MONO_VERSIONS=""
# Future versions
MONO_VERSIONS="$MONO_VERSIONS 6.4=preview-xenial"
MONO_VERSIONS="$MONO_VERSIONS 6.10=preview-xenial"
# Semi-Supported versions
MONO_VERSIONS="$MONO_VERSIONS 6.8 6.6 6.4 6.0"
# Supported versions
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12 5.10 5.8 5.4"
MONO_VERSIONS="$MONO_VERSIONS 5.20 5.18"
# Legacy unsupported versions (but appear to work)
MONO_VERSIONS="$MONO_VERSIONS 5.16 5.14 5.12"
# Legacy unsupported versions
MONO_VERSIONS="$MONO_VERSIONS 5.0"
MONO_VERSIONS="$MONO_VERSIONS 5.10 5.8 5.4 5.0"
#MONO_VERSIONS="$MONO_VERSIONS 4.8=stable-wheezy/snapshots/4.8"
if [ "$opt_version" != "" ]; then
@@ -86,23 +93,29 @@ runOne() {
echo "Finished Test Docker for mono $MONO_VERSION"
}
if [ "$opt_parallel" == "1" ]; then
if [ "$opt_report" != "1" ]; then
if [ "$opt_parallel" == "1" ]; then
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
prepOne "$MONO_VERSION_PAIR"
done
fi
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
prepOne "$MONO_VERSION_PAIR"
if [ "$opt_parallel" == "1" ]; then
runOne "$MONO_VERSION_PAIR" &
else
prepOne "$MONO_VERSION_PAIR"
runOne "$MONO_VERSION_PAIR"
fi
done
if [ "$opt_parallel" == "1" ]; then
echo "Waiting for all runs to finish"
wait
echo "Finished all runs"
fi
fi
for MONO_VERSION_PAIR in $MONO_VERSIONS; do
if [ "$opt_parallel" == "1" ]; then
runOne "$MONO_VERSION_PAIR" &
else
prepOne "$MONO_VERSION_PAIR"
runOne "$MONO_VERSION_PAIR"
fi
done
if [ "$opt_parallel" == "1" ]; then
echo "Waiting for all runs to finish"
wait
echo "Finished all runs"
fi
grep "<test-run" ../../_tests_results/**/*.xml | sed -r 's/.*?mono-([0-9.]+(-s)?).*?_([IU]).*?\.xml.*?failed="([0-9]*)".*/\1\t\3:\tfailed \4/g' | sort -V -t.

View File

@@ -10,8 +10,7 @@ gulp.task('build',
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
'copyImages'
)
)
);

View File

@@ -5,17 +5,6 @@ const cache = require('gulp-cached');
const livereload = require('gulp-livereload');
const paths = require('./helpers/paths.js');
gulp.task('copyJs', () => {
return gulp.src(
[
path.join(paths.src.root, 'polyfills.js')
], { base: paths.src.root })
.pipe(cache('copyJs'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyHtml', () => {
return gulp.src(paths.src.html, { base: paths.src.root })
.pipe(cache('copyHtml'))

View File

@@ -6,17 +6,21 @@ const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const inlineWebWorkers = true;
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
@@ -113,6 +117,22 @@ const config = {
module: {
rules: [
{
test: /\.worker\.js$/,
issuer: {
// monaco-editor includes the editor.worker.js in other language workers,
// don't use worker-loader in that case
exclude: /monaco-editor/
},
use: {
loader: 'worker-loader',
options: {
name: '[name].js',
inline: inlineWebWorkers,
fallback: !inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
@@ -213,6 +233,24 @@ const config = {
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
gulp.task('webpack', () => {
return webpackStream(config)
.pipe(gulp.dest('_output/UI'));

20
frontend/jsconfig.json Normal file
View File

@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es6",
"checkJs": false,
"baseUrl": "src",
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"paths": {
"*": [
"*"
]
}
},
"include": [
"./src/**/*"
],
"exclude": [
]
}

View File

@@ -7,7 +7,7 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -56,7 +56,7 @@ class Blacklist extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -103,7 +103,7 @@ class Blacklist extends Component {
/>
</div>
}
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -232,6 +232,30 @@ function HistoryDetails(props) {
);
}
if (eventType === 'downloadIgnored') {
const {
message
} = data;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title="Name"
data={sourceTitle}
/>
{
!!message &&
<DescriptionListItem
title="Message"
data={message}
/>
}
</DescriptionList>
);
}
return (
<DescriptionList>
<DescriptionListItem

View File

@@ -23,6 +23,8 @@ function getHeaderTitle(eventType) {
return 'Episode File Deleted';
case 'episodeFileRenamed':
return 'Episode File Renamed';
case 'downloadIgnored':
return 'Download Ignored';
default:
return 'Unknown';
}

View File

@@ -8,7 +8,7 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -96,7 +96,7 @@ class History extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
<PageContentBody>
{
isFetchingAny && !isAllPopulated &&
<LoadingIndicator />
@@ -147,7 +147,7 @@ class History extends Component {
/>
</div>
}
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -19,6 +19,8 @@ function getIconName(eventType) {
return icons.DELETE;
case 'episodeFileRenamed':
return icons.ORGANIZE;
case 'downloadIgnored':
return icons.IGNORE;
default:
return icons.UNKNOWN;
}
@@ -47,6 +49,8 @@ function getTooltip(eventType, data) {
return 'Episode file deleted';
case 'episodeFileRenamed':
return 'Episode file renamed';
case 'downloadIgnored':
return 'Episode Download Ignored';
default:
return 'Unknown event';
}

View File

@@ -1,6 +1,7 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
@@ -12,7 +13,7 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
@@ -36,34 +37,26 @@ class Queue extends Component {
lastToggled: null,
selectedState: {},
isPendingSelected: false,
isConfirmRemoveModalOpen: false
isConfirmRemoveModalOpen: false,
items: props.items
};
}
shouldComponentUpdate(nextProps) {
// Don't update when fetching has completed if items have changed,
// before episodes start fetching or when episodes start fetching.
componentDidUpdate(prevProps) {
const {
items,
isEpisodesFetching
} = this.props;
if (
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items) &&
nextProps.items.some((e) => e.episodeId)
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
) {
return false;
}
if (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
@@ -71,7 +64,7 @@ class Queue extends Component {
const selectedIds = this.getSelectedIds();
const isPendingSelected = _.some(this.props.items, (item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
});
if (isPendingSelected !== this.state.isPendingSelected) {
@@ -107,8 +100,8 @@ class Queue extends Component {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = (blacklist) => {
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
onRemoveSelectedConfirmed = (payload) => {
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
}
@@ -124,7 +117,6 @@ class Queue extends Component {
isFetching,
isPopulated,
error,
items,
isEpisodesFetching,
isEpisodesPopulated,
episodesError,
@@ -132,7 +124,7 @@ class Queue extends Component {
totalRecords,
isGrabbing,
isRemoving,
isCheckForFinishedDownloadExecuting,
isRefreshMonitoredDownloadsExecuting,
onRefreshPress,
...otherProps
} = this.props;
@@ -142,13 +134,15 @@ class Queue extends Component {
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isPendingSelected
isPendingSelected,
items
} = this.state;
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
const hasError = error || episodesError;
const selectedCount = this.getSelectedIds().length;
const selectedIds = this.getSelectedIds();
const selectedCount = selectedIds.length;
const disableSelectedActions = selectedCount === 0;
return (
@@ -197,7 +191,7 @@ class Queue extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>
<PageContentBody>
{
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
@@ -254,11 +248,18 @@ class Queue extends Component {
/>
</div>
}
</PageContentBodyConnector>
</PageContentBody>
<RemoveQueueItemsModal
isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount}
canIgnore={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.seriesId && item.episodeId);
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>
@@ -279,7 +280,7 @@ Queue.propTypes = {
totalRecords: PropTypes.number,
isGrabbing: PropTypes.bool.isRequired,
isRemoving: PropTypes.bool.isRequired,
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onGrabSelectedPress: PropTypes.func.isRequired,
onRemoveSelectedPress: PropTypes.func.isRequired

View File

@@ -18,13 +18,13 @@ function createMapStateToProps() {
(state) => state.episodes,
(state) => state.queue.options,
(state) => state.queue.paged,
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => {
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
return {
isEpisodesFetching: episodes.isFetching,
isEpisodesPopulated: episodes.isPopulated,
episodesError: episodes.error,
isCheckForFinishedDownloadExecuting,
isRefreshMonitoredDownloadsExecuting,
...options,
...queue
};
@@ -129,7 +129,7 @@ class QueueConnector extends Component {
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
name: commandNames.REFRESH_MONITORED_DOWNLOADS
});
}
@@ -137,8 +137,8 @@ class QueueConnector extends Component {
this.props.grabQueueItems({ ids });
}
onRemoveSelectedPress = (ids, blacklist) => {
this.props.removeQueueItems({ ids, blacklist });
onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload);
}
//

View File

@@ -10,13 +10,13 @@ function QueueDetails(props) {
size,
sizeleft,
estimatedCompletionTime,
status: queueStatus,
status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage,
progressBar
} = props;
const status = queueStatus.toLowerCase();
const progress = (100 - sizeleft / size * 100);
if (status === 'pending') {
@@ -39,7 +39,35 @@ function QueueDetails(props) {
);
}
// TODO: show an icon when download is complete, but not imported yet?
if (trackedDownloadStatus === 'warning') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.WARNING}
title={'Downloaded - Unable to Import: check logs for details'}
/>
);
}
if (trackedDownloadState === 'importPending') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Waiting to Import'}
/>
);
}
if (trackedDownloadState === 'importing') {
return (
<Icon
name={icons.DOWNLOAD}
kind={kinds.PURPLE}
title={'Downloaded - Importing'}
/>
);
}
}
if (errorMessage) {
@@ -90,6 +118,8 @@ QueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
progressBar: PropTypes.node.isRequired
};

View File

@@ -68,6 +68,7 @@ class QueueRow extends Component {
title,
status,
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage,
series,
@@ -100,8 +101,8 @@ class QueueRow extends Component {
} = this.state;
const progress = 100 - (sizeleft / size * 100);
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
return (
<TableRow>
@@ -129,6 +130,7 @@ class QueueRow extends Component {
sourceTitle={title}
status={status}
trackedDownloadStatus={trackedDownloadStatus}
trackedDownloadState={trackedDownloadState}
statusMessages={statusMessages}
errorMessage={errorMessage}
/>
@@ -220,9 +222,13 @@ class QueueRow extends Component {
if (name === 'quality') {
return (
<TableRowCell key={name}>
<EpisodeQuality
quality={quality}
/>
{
quality ?
<EpisodeQuality
quality={quality}
/> :
null
}
</TableRowCell>
);
}
@@ -350,6 +356,7 @@ class QueueRow extends Component {
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!series}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
@@ -365,6 +372,7 @@ QueueRow.propTypes = {
title: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string,
trackedDownloadState: PropTypes.string,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string,
series: PropTypes.object,

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -15,11 +14,11 @@ function createMapStateToProps() {
createEpisodeSelector(),
createUISettingsSelector(),
(series, episode, uiSettings) => {
const result = _.pick(uiSettings, [
'showRelativeDates',
'shortDateFormat',
'timeFormat'
]);
const result = {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat
};
result.series = series;
result.episode = episode;
@@ -43,8 +42,8 @@ class QueueRowConnector extends Component {
this.props.grabQueueItem({ id: this.props.id });
}
onRemoveQueueItemPress = (blacklist) => {
this.props.removeQueueItem({ id: this.props.id, blacklist });
onRemoveQueueItemPress = (payload) => {
this.props.removeQueueItem({ id: this.props.id, ...payload });
}
//

View File

@@ -37,63 +37,79 @@ function QueueStatusCell(props) {
const {
sourceTitle,
status,
trackedDownloadStatus = 'Ok',
trackedDownloadStatus,
trackedDownloadState,
statusMessages,
errorMessage
} = props;
const hasWarning = trackedDownloadStatus === 'Warning';
const hasError = trackedDownloadStatus === 'Error';
const hasWarning = trackedDownloadStatus === 'warning';
const hasError = trackedDownloadStatus === 'error';
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let title = 'Downloading';
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'Paused') {
if (status === 'paused') {
iconName = icons.PAUSED;
title = 'Paused';
}
if (status === 'Queued') {
if (status === 'queued') {
iconName = icons.QUEUED;
title = 'Queued';
}
if (status === 'Completed') {
if (status === 'completed') {
iconName = icons.DOWNLOADED;
title = 'Downloaded';
if (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ' - Importing';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
iconKind = kinds.DANGER;
}
}
if (status === 'Delay') {
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'delay') {
iconName = icons.PENDING;
title = 'Pending';
}
if (status === 'DownloadClientUnavailable') {
if (status === 'downloadClientUnavailable') {
iconName = icons.PENDING;
iconKind = kinds.WARNING;
title = 'Pending - Download client is unavailable';
}
if (status === 'Failed') {
if (status === 'failed') {
iconName = icons.DOWNLOADING;
iconKind = kinds.DANGER;
title = 'Download failed';
}
if (status === 'Warning') {
if (status === 'warning') {
iconName = icons.DOWNLOADING;
iconKind = kinds.WARNING;
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
}
if (hasError) {
if (status === 'Completed') {
if (status === 'completed') {
iconName = icons.DOWNLOAD;
iconKind = kinds.DANGER;
title = `Import failed: ${sourceTitle}`;
@@ -125,9 +141,15 @@ function QueueStatusCell(props) {
QueueStatusCell.propTypes = {
sourceTitle: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string,
trackedDownloadStatus: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
statusMessages: PropTypes.arrayOf(PropTypes.object),
errorMessage: PropTypes.string
};
QueueStatusCell.defaultProps = {
trackedDownloadStatus: 'Ok',
trackedDownloadState: 'Downloading'
};
export default QueueStatusCell;

View File

@@ -1,4 +0,0 @@
.messageRemove {
margin-bottom: 30px;
color: $dangerColor;
}

View File

@@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import styles from './RemoveQueueItemModal.css';
class RemoveQueueItemModal extends Component {
@@ -21,26 +20,41 @@ class RemoveQueueItemModal extends Component {
super(props, context);
this.state = {
remove: true,
blacklist: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blacklist: false
});
}
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveQueueItemConfirmed = () => {
const blacklist = this.state.blacklist;
onRemoveConfirmed = () => {
const state = this.state;
this.setState({ blacklist: false });
this.props.onRemovePress(blacklist);
this.resetState();
this.props.onRemovePress(state);
}
onModalClose = () => {
this.setState({ blacklist: false });
this.resetState();
this.props.onModalClose();
}
@@ -50,10 +64,11 @@ class RemoveQueueItemModal extends Component {
render() {
const {
isOpen,
sourceTitle
sourceTitle,
canIgnore
} = this.props;
const blacklist = this.state.blacklist;
const { remove, blacklist } = this.state;
return (
<Modal
@@ -73,17 +88,27 @@ class RemoveQueueItemModal extends Component {
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
<div className={styles.messageRemove}>
Removing will remove the download and the file(s) from the download client.
</div>
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText="Prevents Sonarr from automatically grabbing this episode again"
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
onChange={this.onBlacklistChange}
/>
</FormGroup>
@@ -97,7 +122,7 @@ class RemoveQueueItemModal extends Component {
<Button
kind={kinds.DANGER}
onPress={this.onRemoveQueueItemConfirmed}
onPress={this.onRemoveConfirmed}
>
Remove
</Button>
@@ -111,6 +136,7 @@ class RemoveQueueItemModal extends Component {
RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -21,26 +21,41 @@ class RemoveQueueItemsModal extends Component {
super(props, context);
this.state = {
remove: true,
blacklist: false
};
}
//
// Listeners
// Control
resetState = function() {
this.setState({
remove: true,
blacklist: false
});
}
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveQueueItemConfirmed = () => {
const blacklist = this.state.blacklist;
onRemoveConfirmed = () => {
const state = this.state;
this.setState({ blacklist: false });
this.props.onRemovePress(blacklist);
this.resetState();
this.props.onRemovePress(state);
}
onModalClose = () => {
this.setState({ blacklist: false });
this.resetState();
this.props.onModalClose();
}
@@ -50,10 +65,11 @@ class RemoveQueueItemsModal extends Component {
render() {
const {
isOpen,
selectedCount
selectedCount,
canIgnore
} = this.props;
const blacklist = this.state.blacklist;
const { remove, blacklist } = this.state;
return (
<Modal
@@ -74,7 +90,23 @@ class RemoveQueueItemsModal extends Component {
</div>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
@@ -93,7 +125,7 @@ class RemoveQueueItemsModal extends Component {
<Button
kind={kinds.DANGER}
onPress={this.onRemoveQueueItemConfirmed}
onPress={this.onRemoveConfirmed}
>
Remove
</Button>
@@ -107,6 +139,7 @@ class RemoveQueueItemsModal extends Component {
RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -19,7 +19,7 @@ function TimeleftCell(props) {
timeFormat
} = props;
if (status === 'Delay') {
if (status === 'delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
@@ -33,7 +33,7 @@ function TimeleftCell(props) {
);
}
if (status === 'DownloadClientUnavailable') {
if (status === 'downloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
@@ -47,7 +47,7 @@ function TimeleftCell(props) {
);
}
if (!timeleft) {
if (!timeleft || status === 'completed' || status === 'failed') {
return (
<TableRowCell className={styles.timeleft}>
-

View File

@@ -8,7 +8,7 @@ import Icon from 'Components/Icon';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import TextInput from 'Components/Form/TextInput';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
import styles from './AddNewSeries.css';
@@ -88,7 +88,7 @@ class AddNewSeries extends Component {
return (
<PageContent title="Add New Series">
<PageContentBodyConnector>
<PageContentBody>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon
@@ -191,7 +191,7 @@ class AddNewSeries extends Component {
}
<div />
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -14,6 +14,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import Popover from 'Components/Tooltip/Popover';
import SeriesPoster from 'Series/SeriesPoster';
import * as seriesTypes from 'Utilities/Series/seriesTypes';
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
import styles from './AddNewSeriesModalContent.css';
@@ -27,10 +28,19 @@ class AddNewSeriesModalContent extends Component {
super(props, context);
this.state = {
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
props.seriesType.value :
props.initialSeriesType,
searchForMissingEpisodes: false
};
}
componentDidUpdate(prevProps) {
if (this.props.seriesType.value !== prevProps.seriesType.value) {
this.setState({ seriesType: this.props.seriesType.value });
}
}
//
// Listeners
@@ -47,7 +57,12 @@ class AddNewSeriesModalContent extends Component {
}
onAddSeriesPress = () => {
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
const {
searchForMissingEpisodes,
seriesType
} = this.state;
this.props.onAddSeriesPress(searchForMissingEpisodes, seriesType);
}
//
@@ -200,6 +215,7 @@ class AddNewSeriesModalContent extends Component {
name="seriesType"
onChange={onInputChange}
{...seriesType}
value={this.state.seriesType}
/>
</FormGroup>
@@ -262,6 +278,7 @@ AddNewSeriesModalContent.propTypes = {
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
overview: PropTypes.string,
initialSeriesType: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object,

View File

@@ -55,14 +55,13 @@ class AddNewSeriesModalContentConnector extends Component {
this.props.setAddSeriesDefault({ [name]: value });
}
onAddSeriesPress = (searchForMissingEpisodes) => {
onAddSeriesPress = (searchForMissingEpisodes, seriesType) => {
const {
tvdbId,
rootFolderPath,
monitor,
qualityProfileId,
languageProfileId,
seriesType,
seasonFolder,
tags
} = this.props;
@@ -73,7 +72,7 @@ class AddNewSeriesModalContentConnector extends Component {
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
languageProfileId: languageProfileId.value,
seriesType: seriesType.value,
seriesType,
seasonFolder: seasonFolder.value,
tags: tags.value,
searchForMissingEpisodes

View File

@@ -62,6 +62,7 @@
.alreadyExistsIcon {
margin-left: 10px;
color: #37bc9b;
pointer-events: all;
}
.overview {

View File

@@ -58,6 +58,7 @@ class AddNewSeriesSearchResult extends Component {
statistics,
ratings,
folder,
seriesType,
images,
isExistingSeries,
isSmallScreen
@@ -165,6 +166,17 @@ class AddNewSeriesSearchResult extends Component {
</Label> :
null
}
{
status === 'upcoming' ?
<Label
kind={kinds.INFO}
size={sizes.LARGE}
>
Upcoming
</Label> :
null
}
</div>
<div className={styles.overview}>
@@ -180,6 +192,7 @@ class AddNewSeriesSearchResult extends Component {
year={year}
overview={overview}
folder={folder}
initialSeriesType={seriesType}
images={images}
onModalClose={this.onAddSeriesModalClose}
/>
@@ -199,6 +212,7 @@ AddNewSeriesSearchResult.propTypes = {
statistics: PropTypes.object.isRequired,
ratings: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
seriesType: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isExistingSeries: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired

View File

@@ -5,7 +5,7 @@ import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
@@ -21,17 +21,15 @@ class ImportSeries extends Component {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
contentBody: null,
scrollTop: 0
selectedState: {}
};
}
//
// Control
setContentBodyRef = (ref) => {
this.setState({ contentBody: ref });
setScrollerRef = (ref) => {
this.setState({ scroller: ref });
}
//
@@ -94,13 +92,13 @@ class ImportSeries extends Component {
allSelected,
allUnselected,
selectedState,
contentBody
scroller
} = this.state;
return (
<PageContent title="Import Series">
<PageContentBodyConnector
ref={this.setContentBodyRef}
<PageContentBody
registerScroller={this.setScrollerRef}
onScroll={this.onScroll}
>
{
@@ -121,23 +119,21 @@ class ImportSeries extends Component {
}
{
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
<ImportSeriesTableConnector
rootFolderId={rootFolderId}
unmappedFolders={unmappedFolders}
allSelected={allSelected}
allUnselected={allUnselected}
selectedState={selectedState}
contentBody={contentBody}
scroller={scroller}
showLanguageProfile={showLanguageProfile}
scrollTop={this.state.scrollTop}
onSelectAllChange={this.onSelectAllChange}
onSelectedChange={this.onSelectedChange}
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
onScroll={this.onScroll}
/>
}
</PageContentBodyConnector>
</PageContentBody>
{
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes } from 'Helpers/Props';
import FormInputGroup from 'Components/Form/FormInputGroup';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
@@ -10,7 +9,6 @@ import styles from './ImportSeriesRow.css';
function ImportSeriesRow(props) {
const {
style,
id,
monitor,
qualityProfileId,
@@ -26,7 +24,7 @@ function ImportSeriesRow(props) {
} = props;
return (
<VirtualTableRow style={style}>
<>
<VirtualTableSelectCell
inputClassName={styles.selectInput}
id={id}
@@ -90,14 +88,14 @@ function ImportSeriesRow(props) {
<ImportSeriesSelectSeriesConnector
id={id}
isExistingSeries={isExistingSeries}
onInputChange={onInputChange}
/>
</VirtualTableRowCell>
</VirtualTableRow>
</>
);
}
ImportSeriesRow.propTypes = {
style: PropTypes.object.isRequired,
id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired,
qualityProfileId: PropTypes.number.isRequired,

View File

@@ -2,6 +2,7 @@ import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import VirtualTable from 'Components/Table/VirtualTable';
import VirtualTableRow from 'Components/Table/VirtualTableRow';
import ImportSeriesHeader from './ImportSeriesHeader';
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
@@ -112,15 +113,19 @@ class ImportSeriesTable extends Component {
const item = items[rowIndex];
return (
<ImportSeriesRowConnector
<VirtualTableRow
key={key}
style={style}
rootFolderId={rootFolderId}
showLanguageProfile={showLanguageProfile}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
>
<ImportSeriesRowConnector
key={item.id}
rootFolderId={rootFolderId}
showLanguageProfile={showLanguageProfile}
isSelected={selectedState[item.id]}
onSelectedChange={onSelectedChange}
id={item.id}
/>
</VirtualTableRow>
);
}
@@ -133,12 +138,10 @@ class ImportSeriesTable extends Component {
allSelected,
allUnselected,
isSmallScreen,
contentBody,
scroller,
showLanguageProfile,
scrollTop,
selectedState,
onSelectAllChange,
onScroll
onSelectAllChange
} = this.props;
if (!items.length) {
@@ -148,10 +151,9 @@ class ImportSeriesTable extends Component {
return (
<VirtualTable
items={items}
contentBody={contentBody}
scroller={scroller}
isSmallScreen={isSmallScreen}
rowHeight={52}
scrollTop={scrollTop}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
@@ -163,7 +165,6 @@ class ImportSeriesTable extends Component {
/>
}
selectedState={selectedState}
onScroll={onScroll}
/>
);
}
@@ -183,15 +184,13 @@ ImportSeriesTable.propTypes = {
selectedState: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
allSeries: PropTypes.arrayOf(PropTypes.object),
contentBody: PropTypes.object.isRequired,
scroller: PropTypes.instanceOf(Element).isRequired,
showLanguageProfile: PropTypes.bool.isRequired,
scrollTop: PropTypes.number.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemoveSelectedStateItem: PropTypes.func.isRequired,
onSeriesLookup: PropTypes.func.isRequired,
onSetImportSeriesValue: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
onSetImportSeriesValue: PropTypes.func.isRequired
};
export default ImportSeriesTable;

View File

@@ -1,10 +1,10 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
import * as seriesTypes from 'Utilities/Series/seriesTypes';
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
function createMapStateToProps() {
@@ -41,13 +41,23 @@ class ImportSeriesSelectSeriesConnector extends Component {
onSeriesSelect = (tvdbId) => {
const {
id,
items
items,
onInputChange
} = this.props;
const selectedSeries = items.find((item) => item.tvdbId === tvdbId);
this.props.setImportSeriesValue({
id,
selectedSeries: _.find(items, { tvdbId })
selectedSeries
});
if (selectedSeries.seriesType !== seriesTypes.STANDARD) {
onInputChange({
name: 'seriesType',
value: selectedSeries.seriesType
});
}
}
//
@@ -69,6 +79,7 @@ ImportSeriesSelectSeriesConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object),
selectedSeries: PropTypes.object,
isSelected: PropTypes.bool,
onInputChange: PropTypes.func.isRequired,
queueLookupSeries: PropTypes.func.isRequired,
setImportSeriesValue: PropTypes.func.isRequired
};

View File

@@ -7,7 +7,7 @@ import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import RootFolders from 'RootFolder/RootFolders';
import styles from './ImportSeriesSelectFolder.css';
@@ -53,7 +53,7 @@ class ImportSeriesSelectFolder extends Component {
return (
<PageContent title="Import Series">
<PageContentBodyConnector>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
@@ -132,7 +132,7 @@ class ImportSeriesSelectFolder extends Component {
/>
</div>
}
</PageContentBodyConnector>
</PageContentBody>
</PageContent>
);
}

View File

@@ -33,6 +33,7 @@ import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
import Logs from 'System/Logs/Logs';
import Diagnostic from 'Diagnostic/Diagnostic';
function AppRoutes(props) {
const {
@@ -229,6 +230,15 @@ function AppRoutes(props) {
component={Logs}
/>
{/*
Diagnostics
*/}
<Route
path="/diag"
component={Diagnostic}
/>
{/*
Not Found
*/}

View File

@@ -3,9 +3,10 @@ import React, { Component } from 'react';
import { align, icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
import Measure from 'Components/Measure';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import NoSeries from 'Series/NoSeries';
@@ -76,8 +77,10 @@ class CalendarPage extends Component {
filters,
hasSeries,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing,
useCurrentPage,
onRssSyncPress,
onFilterSelect
} = this.props;
@@ -99,6 +102,15 @@ class CalendarPage extends Component {
onPress={this.onGetCalendarLinkPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label="RSS Sync"
iconName={icons.RSS}
isSpinning={isRssSyncExecuting}
onPress={onRssSyncPress}
/>
<PageToolbarButton
label="Search for Missing"
iconName={icons.SEARCH}
@@ -126,7 +138,7 @@ class CalendarPage extends Component {
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector
<PageContentBody
className={styles.calendarPageBody}
innerClassName={styles.calendarInnerPageBody}
>
@@ -147,7 +159,7 @@ class CalendarPage extends Component {
hasSeries &&
<LegendConnector />
}
</PageContentBodyConnector>
</PageContentBody>
<CalendarLinkModal
isOpen={isCalendarLinkModalOpen}
@@ -168,10 +180,12 @@ CalendarPage.propTypes = {
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasSeries: PropTypes.bool.isRequired,
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired,
isSearchingForMissing: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
onSearchMissingPress: PropTypes.func.isRequired,
onDaysCountChange: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};

View File

@@ -3,11 +3,14 @@ import { createSelector } from 'reselect';
import moment from 'moment';
import { isCommandExecuting } from 'Utilities/Command';
import isBefore from 'Utilities/Date/isBefore';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { executeCommand } from 'Store/Actions/commandActions';
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import CalendarPage from './CalendarPage';
function createMissingEpisodeIdsSelector() {
@@ -59,6 +62,7 @@ function createMapStateToProps() {
createSeriesCountSelector(),
createUISettingsSelector(),
createMissingEpisodeIdsSelector(),
createCommandExecutingSelector(commandNames.RSS_SYNC),
createIsSearchingSelector(),
(
selectedFilterKey,
@@ -66,6 +70,7 @@ function createMapStateToProps() {
seriesCount,
uiSettings,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing
) => {
return {
@@ -74,6 +79,7 @@ function createMapStateToProps() {
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasSeries: !!seriesCount,
missingEpisodeIds,
isRssSyncExecuting,
isSearchingForMissing
};
}
@@ -82,6 +88,12 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRssSyncPress() {
dispatch(executeCommand({
name: commandNames.RSS_SYNC
}));
},
onSearchMissingPress(episodeIds) {
dispatch(searchMissing({ episodeIds }));
},

View File

@@ -11,10 +11,12 @@ function CalendarEventQueueDetails(props) {
sizeleft,
estimatedCompletionTime,
status,
trackedDownloadState,
trackedDownloadStatus,
errorMessage
} = props;
const progress = (100 - sizeleft / size * 100);
const progress = size ? (100 - sizeleft / size * 100) : 0;
return (
<QueueDetails
@@ -23,6 +25,8 @@ function CalendarEventQueueDetails(props) {
sizeleft={sizeleft}
estimatedCompletionTime={estimatedCompletionTime}
status={status}
trackedDownloadState={trackedDownloadState}
trackedDownloadStatus={trackedDownloadStatus}
errorMessage={errorMessage}
progressBar={
<div title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}>
@@ -44,6 +48,8 @@ CalendarEventQueueDetails.propTypes = {
sizeleft: PropTypes.number.isRequired,
estimatedCompletionTime: PropTypes.string,
status: PropTypes.string.isRequired,
trackedDownloadState: PropTypes.string.isRequired,
trackedDownloadStatus: PropTypes.string.isRequired,
errorMessage: PropTypes.string
};

View File

@@ -1,6 +1,6 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup';
export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';

View File

@@ -172,7 +172,7 @@ class FilterBuilderModalContent extends Component {
filters.map((filter, index) => {
return (
<FilterBuilderRow
key={index}
key={`${filter.key}-${index}`}
index={index}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}

View File

@@ -135,7 +135,7 @@ class FilterBuilderRowValue extends Component {
tagList={tagList}
allowNew={!tagList.length}
kind={kinds.DEFAULT}
delimiters={[9, 13]}
delimiters={['Tab', 'Enter']}
maxSuggestionsLength={100}
minQueryLength={0}
tagComponent={FilterBuilderRowValueTag}

View File

@@ -1,15 +1,16 @@
import React from 'react';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
const seriesStatusList = [
{ id: 'continuing', name: 'Continuing' },
{ id: 'upcoming', name: 'Upcoming' },
{ id: 'ended', name: 'Ended' }
];
function SeriesStatusFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={protocols}
tagList={seriesStatusList}
{...props}
/>
);

View File

@@ -1,19 +1,8 @@
.enhancedSelect {
composes: input from '~Components/Form/Input.css';
composes: link from '~Components/Link/Link.css';
position: relative;
display: flex;
align-items: center;
padding: 6px 16px;
width: 100%;
height: 35px;
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
color: $black;
cursor: default;
}
.hasError {
@@ -56,6 +45,7 @@
display: flex;
justify-content: center;
max-width: 90%;
max-height: 100%;
width: 350px !important;
height: auto !important;
}

View File

@@ -63,7 +63,7 @@ class EnhancedSelectInputOption extends Component {
EnhancedSelectInputOption.propTypes = {
className: PropTypes.string.isRequired,
id: PropTypes.string.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
isSelected: PropTypes.bool.isRequired,
isDisabled: PropTypes.bool.isRequired,
isHidden: PropTypes.bool.isRequired,

View File

@@ -14,6 +14,7 @@ import PasswordInput from './PasswordInput';
import PathInputConnector from './PathInputConnector';
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
@@ -61,6 +62,9 @@ function getComponent(type) {
case inputTypes.LANGUAGE_PROFILE_SELECT:
return LanguageProfileSelectInputConnector;
case inputTypes.INDEXER_SELECT:
return IndexerSelectInputConnector;
case inputTypes.ROOT_FOLDER_SELECT:
return RootFolderSelectInputConnector;

View File

@@ -0,0 +1,96 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import sortByName from 'Utilities/Array/sortByName';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.indexers,
(state, { includeAny }) => includeAny,
(indexers, includeAny) => {
const {
isFetching,
isPopulated,
error,
items
} = indexers;
const values = _.map(items.sort(sortByName), (indexer) => {
return {
key: indexer.id,
value: indexer.name
};
});
if (includeAny) {
values.unshift({
key: 0,
value: '(Any)'
});
}
return {
isFetching,
isPopulated,
error,
values
};
}
);
}
const mapDispatchToProps = {
dispatchFetchIndexers: fetchIndexers
};
class IndexerSelectInputConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isPopulated) {
this.props.dispatchFetchIndexers();
}
}
//
// Listeners
onChange = ({ name, value }) => {
this.props.onChange({ name, value: parseInt(value) });
}
//
// Render
render() {
return (
<EnhancedSelectInput
{...this.props}
onChange={this.onChange}
/>
);
}
}
IndexerSelectInputConnector.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
includeAny: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired,
dispatchFetchIndexers: PropTypes.func.isRequired
};
IndexerSelectInputConnector.defaultProps = {
includeAny: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);

View File

@@ -98,7 +98,9 @@ class KeyValueListInput extends Component {
className,
value,
keyPlaceholder,
valuePlaceholder
valuePlaceholder,
hasError,
hasWarning
} = this.props;
const { isFocused } = this.state;
@@ -106,7 +108,9 @@ class KeyValueListInput extends Component {
return (
<div className={classNames(
className,
isFocused && styles.isFocused
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
>
{

View File

@@ -8,7 +8,16 @@
}
}
.inputWrapper {
flex: 1 0 0;
}
.buttonWrapper {
flex: 0 0 22px;
}
.keyInput,
.valueInput {
width: 100%;
border: none;
}

View File

@@ -63,34 +63,41 @@ class KeyValueListInputItem extends Component {
return (
<div className={styles.itemContainer}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={this.onKeyChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
<div className={styles.inputWrapper}>
<TextInput
className={styles.keyInput}
name="key"
value={keyValue}
placeholder={keyPlaceholder}
onChange={this.onKeyChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={this.onValueChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
<div className={styles.inputWrapper}>
<TextInput
className={styles.valueInput}
name="value"
value={value}
placeholder={valuePlaceholder}
onChange={this.onValueChange}
onFocus={this.onFocus}
onBlur={this.onBlur}
/>
</div>
{
!isNew &&
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={this.onRemovePress}
/>
}
<div className={styles.buttonWrapper}>
{
isNew ?
null :
<IconButton
name={icons.REMOVE}
tabIndex={-1}
onPress={this.onRemovePress}
/>
}
</div>
</div>
);
}

View File

@@ -4,15 +4,16 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import sortByName from 'Utilities/Array/sortByName';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.languageProfiles,
createSortedSectionSelector('settings.languageProfiles', sortByName),
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(languageProfiles, includeNoChange, includeMixed) => {
const values = _.map(languageProfiles.items.sort(sortByName), (languageProfile) => {
const values = _.map(languageProfiles.items, (languageProfile) => {
return {
key: languageProfile.id,
value: languageProfile.name

View File

@@ -4,15 +4,16 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import sortByName from 'Utilities/Array/sortByName';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import SelectInput from './SelectInput';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.qualityProfiles,
createSortedSectionSelector('settings.qualityProfiles', sortByName),
(state, { includeNoChange }) => includeNoChange,
(state, { includeMixed }) => includeMixed,
(qualityProfiles, includeNoChange, includeMixed) => {
const values = _.map(qualityProfiles.items.sort(sortByName), (qualityProfile) => {
const values = _.map(qualityProfiles.items, (qualityProfile) => {
return {
key: qualityProfile.id,
value: qualityProfile.name

View File

@@ -1,11 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import * as seriesTypes from 'Utilities/Series/seriesTypes';
import SelectInput from './SelectInput';
const seriesTypeOptions = [
{ key: 'standard', value: 'Standard' },
{ key: 'daily', value: 'Daily' },
{ key: 'anime', value: 'Anime' }
{ key: seriesTypes.STANDARD, value: 'Standard' },
{ key: seriesTypes.DAILY, value: 'Daily' },
{ key: seriesTypes.ANIME, value: 'Anime' }
];
function SeriesTypeSelectInput(props) {

View File

@@ -12,6 +12,14 @@
}
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from '~Components/Form/Input.css';
}
.internalInput {
flex: 1 1 0%;
margin-left: 3px;

View File

@@ -100,9 +100,9 @@ class TagInput extends Component {
suggestions
} = this.state;
const keyCode = event.keyCode;
const key = event.key;
if (keyCode === 8 && !value.length) {
if (key === 'Backspace' && !value.length) {
const index = tags.length - 1;
if (index >= 0) {
@@ -116,7 +116,7 @@ class TagInput extends Component {
event.preventDefault();
}
if (delimiters.includes(keyCode)) {
if (delimiters.includes(key)) {
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
const tag = getTag(value, selectedIndex, suggestions, allowNew);
@@ -210,6 +210,8 @@ class TagInput extends Component {
const {
className,
inputContainerClassName,
hasError,
hasWarning,
...otherProps
} = this.props;
@@ -226,7 +228,9 @@ class TagInput extends Component {
className={styles.internalInput}
inputContainerClassName={classNames(
inputContainerClassName,
isFocused && styles.isFocused
isFocused && styles.isFocused,
hasError && styles.hasError,
hasWarning && styles.hasWarning
)}
value={value}
suggestions={suggestions}
@@ -256,7 +260,7 @@ TagInput.propTypes = {
allowNew: PropTypes.bool.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
placeholder: PropTypes.string.isRequired,
delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
minQueryLength: PropTypes.number.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
@@ -271,8 +275,7 @@ TagInput.defaultProps = {
allowNew: true,
kind: kinds.INFO,
placeholder: '',
// Tab, enter, space and comma
delimiters: [9, 13, 32, 188],
delimiters: ['Tab', 'Enter', ' ', ','],
minQueryLength: 1,
tagComponent: TagInputTag
};

View File

@@ -25,3 +25,7 @@
.warning {
color: $warningColor;
}
.purple {
color: $purple;
}

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import styles from './LoadingIndicator.css';
function LoadingIndicator({ className, size }) {
function LoadingIndicator({ className, rippleClassName, size }) {
const sizeInPx = `${size}px`;
const width = sizeInPx;
const height = sizeInPx;
@@ -17,17 +17,17 @@ function LoadingIndicator({ className, size }) {
style={{ width, height }}
>
<div
className={styles.ripple}
className={rippleClassName}
style={{ width, height }}
/>
<div
className={styles.ripple}
className={rippleClassName}
style={{ width, height }}
/>
<div
className={styles.ripple}
className={rippleClassName}
style={{ width, height }}
/>
</div>
@@ -37,11 +37,13 @@ function LoadingIndicator({ className, size }) {
LoadingIndicator.propTypes = {
className: PropTypes.string,
rippleClassName: PropTypes.string,
size: PropTypes.number
};
LoadingIndicator.defaultProps = {
className: styles.loading,
rippleClassName: styles.ripple,
size: 50
};

View File

@@ -0,0 +1,45 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
class InlineMarkdown extends Component {
//
// Render
render() {
const {
className,
data
} = this.props;
// For now only replace links
const markdownBlocks = [];
if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
while ((match = regex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
}
return <span className={className}>{markdownBlocks}</span>;
}
}
InlineMarkdown.propTypes = {
className: PropTypes.string,
data: PropTypes.string
};
export default InlineMarkdown;

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import React, { useEffect } from 'react';
import { kinds, sizes } from 'Helpers/Props';
import keyboardShortcuts from 'Components/keyboardShortcuts';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal';
@@ -21,9 +22,19 @@ function ConfirmModal(props) {
hideCancelButton,
isSpinning,
onConfirm,
onCancel
onCancel,
bindShortcut,
unbindShortcut
} = props;
useEffect(() => {
if (isOpen) {
bindShortcut('enter', onConfirm);
} else {
unbindShortcut('enter', onConfirm);
}
}, [onConfirm]);
return (
<Modal
isOpen={isOpen}
@@ -49,7 +60,7 @@ function ConfirmModal(props) {
}
<SpinnerButton
data-autofocus={true}
autoFocus={true}
kind={kind}
isSpinning={isSpinning}
onPress={onConfirm}
@@ -74,7 +85,9 @@ ConfirmModal.propTypes = {
hideCancelButton: PropTypes.bool,
isSpinning: PropTypes.bool.isRequired,
onConfirm: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired
onCancel: PropTypes.func.isRequired,
bindShortcut: PropTypes.func.isRequired,
unbindShortcut: PropTypes.func.isRequired
};
ConfirmModal.defaultProps = {
@@ -85,4 +98,4 @@ ConfirmModal.defaultProps = {
isSpinning: false
};
export default ConfirmModal;
export default keyboardShortcuts(ConfirmModal);

View File

@@ -1,6 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock';
import classNames from 'classnames';
import elementClass from 'element-class';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
@@ -181,31 +182,33 @@ class Modal extends Component {
}
return ReactDOM.createPortal(
<div
className={styles.modalContainer}
>
<FocusLock disabled={false}>
<div
ref={this._setBackgroundRef}
className={backdropClassName}
onMouseDown={this.onBackdropBeginPress}
onMouseUp={this.onBackdropEndPress}
className={styles.modalContainer}
>
<div
className={classNames(
className,
styles[size]
)}
style={style}
ref={this._setBackgroundRef}
className={backdropClassName}
onMouseDown={this.onBackdropBeginPress}
onMouseUp={this.onBackdropEndPress}
>
<ErrorBoundary
errorComponent={ModalError}
onModalClose={onModalClose}
<div
className={classNames(
className,
styles[size]
)}
style={style}
>
{children}
</ErrorBoundary>
<ErrorBoundary
errorComponent={ModalError}
onModalClose={onModalClose}
>
{children}
</ErrorBoundary>
</div>
</div>
</div>
</div>,
</FocusLock>,
this._node
);
}

View File

@@ -7,7 +7,7 @@ import styles from './MonitorToggleButton.css';
function getTooltip(monitored, isDisabled) {
if (isDisabled) {
return 'Cannot toogle monitored state when series is unmonitored';
return 'Cannot toggle monitored state when series is unmonitored';
}
if (monitored) {

View File

@@ -3,6 +3,18 @@
align-items: center;
}
.loading {
margin-top: 18px;
margin-bottom: 18px;
text-align: center;
}
.ripple {
composes: ripple from '~Components/Loading/LoadingIndicator.css';
border: 2px solid $toolbarColor;
}
.input {
margin-left: 8px;
width: 200px;

View File

@@ -1,29 +1,18 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import Fuse from 'fuse.js';
import { icons } from 'Helpers/Props';
import Icon from 'Components/Icon';
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
import SeriesSearchResult from './SeriesSearchResult';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FuseWorker from './fuse.worker';
import styles from './SeriesSearchInput.css';
const LOADING_TYPE = 'suggestionsLoading';
const ADD_NEW_TYPE = 'addNew';
const fuseOptions = {
shouldSort: true,
includeMatches: true,
threshold: 0.3,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
'title',
'alternateTitles.title',
'tags.label'
]
};
const workerInstance = new FuseWorker();
class SeriesSearchInput extends Component {
@@ -43,6 +32,7 @@ class SeriesSearchInput extends Component {
componentDidMount() {
this.props.bindShortcut(shortcuts.SERIES_SEARCH_INPUT.key, this.focusInput);
workerInstance.addEventListener('message', this.onSuggestionsReceived, false);
}
//
@@ -82,6 +72,16 @@ class SeriesSearchInput extends Component {
);
}
if (item.type === LOADING_TYPE) {
return (
<LoadingIndicator
className={styles.loading}
rippleClassName={styles.ripple}
size={30}
/>
);
}
return (
<SeriesSearchResult
{...item.item}
@@ -154,35 +154,30 @@ class SeriesSearchInput extends Component {
}
onSuggestionsFetchRequested = ({ value }) => {
const { series } = this.props;
let suggestions = [];
if (value.length === 1) {
suggestions = series.reduce((acc, s) => {
if (s.firstCharacter === value.toLowerCase()) {
acc.push({
item: s,
indices: [
[0, 0]
],
matches: [
{
value: s.title,
key: 'title'
}
],
arrayIndex: 0
});
this.setState({
suggestions: [
{
type: LOADING_TYPE,
title: value
}
]
});
this.requestSuggestions(value);
};
return acc;
}, []);
} else {
const fuse = new Fuse(series, fuseOptions);
suggestions = fuse.search(value);
}
requestSuggestions = _.debounce((value) => {
const payload = {
value,
series: this.props.series
};
this.setState({ suggestions });
workerInstance.postMessage(payload);
}, 250);
onSuggestionsReceived = (message) => {
this.setState({
suggestions: message.data
});
}
onSuggestionsClearRequested = () => {

View File

@@ -0,0 +1,63 @@
import Fuse from 'fuse.js';
const fuseOptions = {
shouldSort: true,
includeMatches: true,
threshold: 0.3,
location: 0,
distance: 100,
maxPatternLength: 32,
minMatchCharLength: 1,
keys: [
'title',
'alternateTitles.title',
'tags.label'
]
};
function getSuggestions(series, value) {
const limit = 10;
let suggestions = [];
if (value.length === 1) {
for (let i = 0; i < series.length; i++) {
const s = series[i];
if (s.firstCharacter === value.toLowerCase()) {
suggestions.push({
item: series[i],
indices: [
[0, 0]
],
matches: [
{
value: s.title,
key: 'title'
}
],
arrayIndex: 0
});
if (suggestions.length > limit) {
break;
}
}
}
} else {
const fuse = new Fuse(series, fuseOptions);
suggestions = fuse.search(value, { limit });
}
return suggestions;
}
self.addEventListener('message', (e) => {
if (!e) {
return;
}
const {
series,
value
} = e.data;
self.postMessage(getSuggestions(series, value));
});

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import { isLocked } from 'Utilities/scrollLock';
import { scrollDirections } from 'Helpers/Props';
import OverlayScroller from 'Components/Scroller/OverlayScroller';
@@ -8,6 +9,15 @@ import styles from './PageContentBody.css';
class PageContentBody extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._isMobile = isMobileUtil();
}
//
// Listeners
@@ -26,13 +36,12 @@ class PageContentBody extends Component {
const {
className,
innerClassName,
isSmallScreen,
children,
dispatch,
...otherProps
} = this.props;
const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller;
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
return (
<ScrollerComponent
@@ -52,7 +61,6 @@ class PageContentBody extends Component {
PageContentBody.propTypes = {
className: PropTypes.string,
innerClassName: PropTypes.string,
isSmallScreen: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onScroll: PropTypes.func,
dispatch: PropTypes.func

View File

@@ -1,17 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import PageContentBody from './PageContentBody';
function createMapStateToProps() {
return createSelector(
createDimensionsSelector(),
(dimensions) => {
return {
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
export default connect(createMapStateToProps, null, null, { forwardRef: true })(PageContentBody);

View File

@@ -1,17 +1,17 @@
import React from 'react';
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
import PageContentBodyConnector from './PageContentBodyConnector';
import PageContentBody from './PageContentBody';
import styles from './PageContentError.css';
function PageContentError(props) {
return (
<div className={styles.content}>
<PageContentBodyConnector>
<PageContentBody>
<ErrorBoundaryError
{...props}
message='There was an error loading this page'
/>
</PageContentBodyConnector>
</PageContentBody>
</div>
);
}

View File

@@ -1,4 +1,3 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import dimensions from 'Styles/Variables/dimensions';
@@ -18,7 +17,7 @@ class PageJumpBar extends Component {
this.state = {
height: 0,
visibleItems: props.items
visibleItems: props.items.order
};
}
@@ -52,29 +51,47 @@ class PageJumpBar extends Component {
minimumItems
} = this.props;
if (!items) {
return;
}
const {
characters,
order
} = items;
const height = this.state.height;
const maximumItems = Math.floor(height / ITEM_HEIGHT);
const diff = items.length - maximumItems;
const diff = order.length - maximumItems;
if (diff < 0) {
this.setState({ visibleItems: items });
this.setState({ visibleItems: order });
return;
}
if (items.length < minimumItems) {
this.setState({ visibleItems: items });
if (order.length < minimumItems) {
this.setState({ visibleItems: order });
return;
}
const removeDiff = Math.ceil(items.length / maximumItems);
// get first, last, and most common in between to make up numbers
const visibleItems = [order[0]];
const visibleItems = _.reduce(items, (acc, item, index) => {
if (index % removeDiff === 0) {
acc.push(item);
const sorted = order.slice(1, -1).map((x) => characters[x]).sort((a, b) => b - a);
const minCount = sorted[maximumItems - 3];
const greater = sorted.reduce((acc, value) => acc + (value > minCount ? 1 : 0), 0);
let minAllowed = maximumItems - 2 - greater;
for (let i = 1; i < order.length - 1; i++) {
if (characters[order[i]] > minCount) {
visibleItems.push(order[i]);
} else if (characters[order[i]] === minCount && minAllowed > 0) {
visibleItems.push(order[i]);
minAllowed--;
}
}
return acc;
}, []);
visibleItems.push(order[order.length - 1]);
this.setState({ visibleItems });
}
@@ -129,7 +146,7 @@ class PageJumpBar extends Component {
}
PageJumpBar.propTypes = {
items: PropTypes.arrayOf(PropTypes.string).isRequired,
items: PropTypes.object.isRequired,
minimumItems: PropTypes.number.isRequired,
onItemPress: PropTypes.func.isRequired
};

View File

@@ -1,5 +1,5 @@
.jumpBarItem {
flex: 1 0 $jumpBarItemHeight;
flex: 1 1 $jumpBarItemHeight;
border-bottom: 1px solid $borderColor;
text-align: center;
font-weight: bold;

View File

@@ -165,6 +165,23 @@ const links = [
to: '/system/logs/files'
}
]
},
{
iconName: icons.DEBUG,
hidden: true,
title: 'Diagnostics',
to: '/diag/status',
children: [
{
title: 'Status',
to: '/diag/status'
},
{
title: 'Script Console',
to: '/diag/script'
}
]
}
];
@@ -473,6 +490,10 @@ class PageSidebar extends Component {
const isActiveParent = activeParent === link.to;
const hasActiveChild = hasActiveChildLink(link, pathname);
if (link.hidden && !isActiveParent && !hasActiveChild) {
return null;
}
return (
<PageSidebarItem
key={link.to}

View File

@@ -41,6 +41,7 @@ function PageToolbarButton(props) {
}
PageToolbarButton.propTypes = {
...Link.propTypes,
label: PropTypes.string.isRequired,
iconName: PropTypes.object.isRequired,
spinningName: PropTypes.object,

View File

@@ -1,6 +1,6 @@
.sectionContainer {
display: flex;
flex: 1 1 10%;
flex: 1 1 auto;
overflow: hidden;
}

View File

@@ -15,7 +15,6 @@ import styles from './PageToolbarSection.css';
const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
const SEPARATOR_NAME = 'PageToolbarSeparator';
function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
let buttonCount = 0;
@@ -23,9 +22,7 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
const validChildren = [];
forEach(children, (child) => {
const name = child.type.name;
if (name === SEPARATOR_NAME) {
if (Object.keys(child.props).length === 0) {
separatorCount++;
} else {
buttonCount++;
@@ -68,12 +65,14 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
}
validChildren.forEach((child, index) => {
const isSeparator = Object.keys(child.props).length === 0;
if (actualButtons < maxButtons) {
if (child.type.name !== SEPARATOR_NAME) {
if (!isSeparator) {
buttons.push(child);
actualButtons++;
}
} else if (child.type.name !== SEPARATOR_NAME) {
} else if (!isSeparator) {
overflowItems.push(child.props);
}
});

View File

@@ -2,6 +2,10 @@
/* Placeholder */
}
.track {
/* Placeholder */
}
.thumb {
min-height: 100px;
border: 1px solid transparent;

View File

@@ -37,6 +37,10 @@ class OverlayScroller extends Component {
_setScrollRef = (ref) => {
this._scroller = ref;
if (ref) {
this.props.registerScroller(ref.view);
}
}
_renderThumb = (props) => {
@@ -60,6 +64,7 @@ class OverlayScroller extends Component {
return (
<div
className={styles.track}
style={finalStyle}
{...props}
/>
@@ -78,6 +83,7 @@ class OverlayScroller extends Component {
return (
<div
className={styles.track}
style={finalStyle}
{...props}
/>
@@ -157,7 +163,8 @@ OverlayScroller.propTypes = {
autoHide: PropTypes.bool.isRequired,
autoScroll: PropTypes.bool.isRequired,
children: PropTypes.node,
onScroll: PropTypes.func
onScroll: PropTypes.func,
registerScroller: PropTypes.func
};
OverlayScroller.defaultProps = {
@@ -165,7 +172,8 @@ OverlayScroller.defaultProps = {
trackClassName: styles.thumb,
scrollDirection: scrollDirections.VERTICAL,
autoHide: false,
autoScroll: true
autoScroll: true,
registerScroller: () => {}
};
export default OverlayScroller;

View File

@@ -17,12 +17,18 @@ class Scroller extends Component {
componentDidMount() {
const {
scrollDirection,
autoFocus,
scrollTop
} = this.props;
if (this.props.scrollTop != null) {
this._scroller.scrollTop = scrollTop;
}
if (autoFocus && scrollDirection !== scrollDirections.NONE) {
this._scroller.focus({ preventScroll: true });
}
}
//
@@ -30,6 +36,8 @@ class Scroller extends Component {
_setScrollerRef = (ref) => {
this._scroller = ref;
this.props.registerScroller(ref);
}
//
@@ -43,6 +51,7 @@ class Scroller extends Component {
children,
scrollTop,
onScroll,
registerScroller,
...otherProps
} = this.props;
@@ -55,6 +64,7 @@ class Scroller extends Component {
styles[scrollDirection],
autoScroll && styles.autoScroll
)}
tabIndex={-1}
{...otherProps}
>
{children}
@@ -67,15 +77,19 @@ class Scroller extends Component {
Scroller.propTypes = {
className: PropTypes.string,
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
autoFocus: PropTypes.bool.isRequired,
autoScroll: PropTypes.bool.isRequired,
scrollTop: PropTypes.number,
children: PropTypes.node,
onScroll: PropTypes.func
onScroll: PropTypes.func,
registerScroller: PropTypes.func
};
Scroller.defaultProps = {
scrollDirection: scrollDirections.VERTICAL,
autoScroll: true
autoFocus: true,
autoScroll: true,
registerScroller: () => {}
};
export default Scroller;

View File

@@ -262,7 +262,7 @@ class SignalRConnector extends Component {
}
handleSystemTask = () => {
// No-op for now, we may want this later
this.props.dispatchFetchCommands();
}
handleRootfolder = () => {

View File

@@ -1,3 +1,7 @@
.tableContainer {
width: 100%;
}
.tableBodyContainer {
position: relative;
}

View File

@@ -1,12 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { WindowScroller } from 'react-virtualized';
import { isLocked } from 'Utilities/scrollLock';
import { scrollDirections } from 'Helpers/Props';
import Measure from 'Components/Measure';
import Scroller from 'Components/Scroller/Scroller';
import VirtualTableBody from './VirtualTableBody';
import { WindowScroller, Grid } from 'react-virtualized';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import styles from './VirtualTable.css';
const ROW_HEIGHT = 38;
@@ -44,28 +42,37 @@ class VirtualTable extends Component {
width: 0
};
this._isInitialized = false;
this._grid = null;
}
componentDidMount() {
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
}
componentDidUpdate(prevProps, prevState) {
const {
items,
scrollIndex
} = this.props;
componentDidUpdate(prevProps, preState) {
const { scrollIndex, rowHeight } = this.props;
const {
width
} = this.state;
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
const scrollTop = (scrollIndex + 1) * rowHeight + 20;
this.props.onScroll({ scrollTop });
this._grid.scrollToCell({
rowIndex: scrollIndex,
columnIndex: 0
});
}
}
//
// Control
rowGetter = ({ index }) => {
return this.props.items[index];
setGridRef = (ref) => {
this._grid = ref;
}
//
@@ -77,36 +84,18 @@ class VirtualTable extends Component {
});
}
onSectionRendered = () => {
if (!this._isInitialized && this._contentBodyNode) {
this.props.onRender();
this._isInitialized = true;
}
}
onScroll = (props) => {
if (isLocked()) {
return;
}
const { onScroll } = this.props;
onScroll(props);
}
//
// Render
render() {
const {
isSmallScreen,
className,
items,
isSmallScreen,
scroller,
header,
headerHeight,
scrollTop,
rowRenderer,
onScroll,
...otherProps
} = this.props;
@@ -114,66 +103,89 @@ class VirtualTable extends Component {
width
} = this.state;
const gridStyle = {
boxSizing: undefined,
direction: undefined,
height: undefined,
position: undefined,
willChange: undefined,
overflow: undefined,
width: undefined
};
const containerStyle = {
position: undefined
};
return (
<Measure onMeasure={this.onMeasure}>
<WindowScroller
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
onScroll={this.onScroll}
>
{({ height, isScrolling }) => {
return (
<WindowScroller
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return null;
}
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Scroller
className={className}
scrollDirection={scrollDirections.HORIZONTAL}
>
{header}
<VirtualTableBody
autoContainerWidth={true}
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowCount={items.length}
columnCount={1}
scrollTop={scrollTop}
autoHeight={true}
overscanRowCount={2}
cellRenderer={rowRenderer}
columnWidth={width}
overscanIndicesGetter={overscanIndicesGetter}
onSectionRendered={this.onSectionRendered}
{...otherProps}
/>
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
autoContainerWidth={true}
autoHeight={true}
autoWidth={true}
width={width}
height={height}
headerHeight={height - headerHeight}
rowHeight={ROW_HEIGHT}
rowCount={items.length}
columnCount={1}
columnWidth={width}
scrollTop={scrollTop}
onScroll={onChildScroll}
overscanRowCount={2}
cellRenderer={rowRenderer}
overscanIndicesGetter={overscanIndicesGetter}
scrollToAlignment={'start'}
isScrollingOptout={true}
className={styles.tableBodyContainer}
style={gridStyle}
containerStyle={containerStyle}
{...otherProps}
/>
</div>
</Scroller>
);
}
}
</WindowScroller>
</Measure>
</Measure>
);
}
}
</WindowScroller>
);
}
}
VirtualTable.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
className: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
scrollTop: PropTypes.number.isRequired,
scrollIndex: PropTypes.number,
contentBody: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
scroller: PropTypes.instanceOf(Element).isRequired,
header: PropTypes.node.isRequired,
headerHeight: PropTypes.number.isRequired,
rowRenderer: PropTypes.func.isRequired,
rowHeight: PropTypes.number.isRequired,
onRender: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired
rowHeight: PropTypes.number.isRequired
};
VirtualTable.defaultProps = {
className: styles.tableContainer,
headerHeight: 38,
onRender: () => {}
headerHeight: 38
};
export default VirtualTable;

View File

@@ -1,3 +0,0 @@
.tableBodyContainer {
position: relative;
}

View File

@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid } from 'react-virtualized';
import styles from './VirtualTableBody.css';
class VirtualTableBody extends Component {
//
// Render
render() {
return (
<Grid
{...this.props}
style={{
boxSizing: undefined,
direction: undefined,
height: undefined,
position: undefined,
willChange: undefined,
overflow: undefined,
width: undefined
}}
containerStyle={{
position: undefined
}}
/>
);
}
}
VirtualTableBody.propTypes = {
className: PropTypes.string.isRequired
};
VirtualTableBody.defaultProps = {
className: styles.tableBodyContainer
};
export default VirtualTableBody;

View File

@@ -54,9 +54,9 @@ class Tooltip extends Component {
} else if ((/^bottom/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom - 20;
} else if ((/^right/).test(data.placement)) {
data.styles.maxWidth = windowWidth - right - 30;
data.styles.maxWidth = windowWidth - right - 35;
} else {
data.styles.maxWidth = left - 30;
data.styles.maxWidth = left - 35;
}
return data;

View File

@@ -8,6 +8,16 @@ export const shortcuts = {
name: 'Open This Modal'
},
CLOSE_MODAL: {
key: 'Esc',
name: 'Close Current Modal'
},
ACCEPT_CONFIRM_MODAL: {
key: 'Enter',
name: 'Accept Confirmation Modal'
},
SERIES_SEARCH_INPUT: {
key: 's',
name: 'Focus Search Box'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@@ -0,0 +1,44 @@
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
import Switch from 'Components/Router/Switch';
import StatusConnector from './Status/StatusConnector';
import ScriptConnector from './Script/ScriptConnector';
class Diagnostic extends Component {
//
// Render
render() {
return (
<Switch>
<Route
exact={true}
path="/diag/status"
component={StatusConnector}
/>
<Route
exact={true}
path="/diag/script"
component={ScriptConnector}
/>
{/* Redirect root to status */}
<Route
exact={true}
path="/diag"
render={() => {
return (
<Redirect
to={getPathWithUrlBase('/diag/status')}
/>
);
}}
/>
</Switch>
);
}
}
export default Diagnostic;

View File

@@ -0,0 +1,82 @@
import ReactMonacoEditor from 'react-monaco-editor';
import shallowEqual from 'shallowequal';
// All editor features -> 7.56 MiB
// import 'monaco-editor/esm/vs/editor/editor.all';
// Only the needed editor features -> 6.88 MiB
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands';
import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget';
// import 'monaco-editor/esm/vs/editor/browser/widget/diffEditorWidget';
// import 'monaco-editor/esm/vs/editor/browser/widget/diffNavigator';
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching';
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/caretOperations';
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/transpose';
// import 'monaco-editor/esm/vs/editor/contrib/clipboard/clipboard';
// import 'monaco-editor/esm/vs/editor/contrib/codeAction/codeActionContributions';
// import 'monaco-editor/esm/vs/editor/contrib/codelens/codelensController';
// import 'monaco-editor/esm/vs/editor/contrib/colorPicker/colorDetector';
import 'monaco-editor/esm/vs/editor/contrib/comment/comment';
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/contextmenu';
import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/cursorUndo';
import 'monaco-editor/esm/vs/editor/contrib/dnd/dnd';
import 'monaco-editor/esm/vs/editor/contrib/find/findController';
import 'monaco-editor/esm/vs/editor/contrib/folding/folding';
import 'monaco-editor/esm/vs/editor/contrib/fontZoom/fontZoom';
// import 'monaco-editor/esm/vs/editor/contrib/format/formatActions';
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionCommands';
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionMouse';
// import 'monaco-editor/esm/vs/editor/contrib/gotoError/gotoError';
import 'monaco-editor/esm/vs/editor/contrib/hover/hover';
import 'monaco-editor/esm/vs/editor/contrib/inPlaceReplace/inPlaceReplace';
import 'monaco-editor/esm/vs/editor/contrib/linesOperations/linesOperations';
// import 'monaco-editor/esm/vs/editor/contrib/links/links';
import 'monaco-editor/esm/vs/editor/contrib/multicursor/multicursor';
// import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints';
// import 'monaco-editor/esm/vs/editor/contrib/referenceSearch/referenceSearch';
import 'monaco-editor/esm/vs/editor/contrib/rename/rename';
import 'monaco-editor/esm/vs/editor/contrib/smartSelect/smartSelect';
// import 'monaco-editor/esm/vs/editor/contrib/snippet/snippetController2';
// import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController';
// import 'monaco-editor/esm/vs/editor/contrib/tokenization/tokenization';
// import 'monaco-editor/esm/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter';
import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations';
import 'monaco-editor/esm/vs/editor/contrib/wordPartOperations/wordPartOperations';
// csharp&json language
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp';
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp.contribution';
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
import 'monaco-editor/esm/vs/language/json/jsonWorker';
import 'monaco-editor/esm/vs/language/json/jsonMode';
// Create a WebWorker from a blob rather than an url
import * as EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker';
import * as JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker';
self.MonacoEnvironment = {
getWorker: (moduleId, label) => {
if (label === 'editorWorkerService') {
return new EditorWorker();
}
if (label === 'json') {
return new JsonWorker();
}
return null;
}
};
class MonacoEditor extends ReactMonacoEditor {
// ReactMonacoEditor should've been PureComponent
shouldComponentUpdate(nextProps, nextState) {
if (!shallowEqual(this.props, nextProps)) {
return true;
}
return false;
}
}
export default MonacoEditor;

View File

@@ -0,0 +1,93 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchStatus, updateScript, validateScript, executeScript } from 'Store/Actions/diagnosticActions';
import ScriptConsole from './ScriptConsole';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Alert from 'Components/Alert';
import { kinds } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
function createMapStateToProps() {
return createSelector(
(state) => state.diagnostic,
(diag) => {
return {
isStatusPopulated: diag.status.isPopulated,
isScriptConsoleEnabled: diag.status.item.scriptConsoleEnabled,
isExecuting: diag.script.isExecuting || false,
isDebugging: diag.script.isDebugging || false,
isValidating: diag.script.isValidating,
code: diag.script.code,
result: diag.script.result,
validation: diag.script.validation,
error: diag.script.error
};
}
);
}
const mapDispatchToProps = {
fetchStatus,
updateScript,
validateScript,
executeScript
};
class ScriptConnector extends Component {
//
// Lifecycle
componentDidMount() {
if (!this.props.isStatusPopulated) {
this.props.fetchStatus();
}
}
//
// Render
render() {
if (!this.props.isStatusPopulated) {
return (
<PageContent>
<LoadingIndicator />
</PageContent>
);
} else if (!this.props.isScriptConsoleEnabled) {
return (
<PageContent>
<Alert kind={kinds.WARNING}>
Diagnostic Scripting is disabled
</Alert>
</PageContent>
);
}
return (
<ScriptConsole
{...this.props}
/>
);
}
}
ScriptConnector.propTypes = {
isStatusPopulated: PropTypes.bool.isRequired,
isScriptConsoleEnabled: PropTypes.bool,
isExecuting: PropTypes.bool.isRequired,
isDebugging: PropTypes.bool.isRequired,
isValidating: PropTypes.bool.isRequired,
code: PropTypes.string,
result: PropTypes.object,
error: PropTypes.object,
validation: PropTypes.object,
fetchStatus: PropTypes.func.isRequired,
updateScript: PropTypes.func.isRequired,
validateScript: PropTypes.func.isRequired,
executeScript: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ScriptConnector);

View File

@@ -0,0 +1,6 @@
.split {
display: flex;
justify-content: space-between;
overflow: hidden;
height: 100%;
}

View File

@@ -0,0 +1,139 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, lazy, Suspense } from 'react';
import { icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageContent from 'Components/Page/PageContent';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import styles from './ScriptConsole.css';
// Lazy load the Monaco Editor since it's a big bundle
const MonacoEditor = lazy(() => import(/* webpackChunkName: "monaco-editor" */ './MonacoEditor'));
const DefaultOptions = {
selectOnLineNumbers: true,
scrollBeyondLastLine: false
};
const DefaultResultOptions = {
...DefaultOptions,
readOnly: true
};
class ScriptConsole extends Component {
//
// Lifecycle
editorDidMount = (editor, monaco) => {
console.log('editorDidMount', editor);
editor.focus();
this.monaco = monaco;
this.editor = editor;
this.updateValidation(this.props.validation);
}
updateValidation(validation) {
if (!this.monaco) {
return;
}
let diagnostics = [];
if (validation && validation.errorDiagnostics) {
diagnostics = validation.errorDiagnostics;
}
const model = this.editor.getModel();
this.monaco.editor.setModelMarkers(model, 'editor', diagnostics);
}
onChange = (newValue, e) => {
this.props.updateScript({ code: newValue });
this.validateCode();
}
validateCode = _.debounce(() => {
const code = this.props.code;
this.props.validateScript({ code });
}, 250, { leading: false, trailing: true })
onExecuteScriptPress = () => {
const code = this.props.code;
this.props.executeScript({ code });
}
onDebugScriptPress = () => {
const code = this.props.code;
this.props.executeScript({ code, debug: true });
}
//
// Render
render() {
const code = this.props.code;
const result = JSON.stringify(this.props.result, null, 2);
this.updateValidation(this.props.validation);
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Run"
iconName={this.props.isExecuting ? icons.REFRESH : icons.SCRIPT_RUN}
isSpinning={this.props.isExecuting}
onPress={this.onExecuteScriptPress}
/>
<PageToolbarButton
label="Debug"
iconName={this.props.isDebugging ? icons.REFRESH : icons.SCRIPT_DEBUG}
isSpinning={this.props.isDebugging}
onPress={this.onDebugScriptPress}
/>
</PageToolbarSection>
</PageToolbar>
<Suspense fallback={<LoadingIndicator />}>
<div className={styles.split}>
<MonacoEditor
language="csharp"
theme="vs-light"
width="50%"
value={code}
options={DefaultOptions}
onChange={this.onChange}
editorDidMount={this.editorDidMount}
/>
<MonacoEditor
language="json"
theme="vs-light"
width="50%"
value={result}
options={DefaultResultOptions}
/>
</div>
</Suspense>
</PageContent>
);
}
}
ScriptConsole.propTypes = {
isExecuting: PropTypes.bool.isRequired,
isDebugging: PropTypes.bool.isRequired,
isValidating: PropTypes.bool.isRequired,
code: PropTypes.string,
result: PropTypes.object,
error: PropTypes.object,
validation: PropTypes.object,
updateScript: PropTypes.func.isRequired,
validateScript: PropTypes.func.isRequired,
executeScript: PropTypes.func.isRequired
};
export default ScriptConsole;

View File

@@ -0,0 +1,5 @@
.descriptionList {
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
margin-bottom: 10px;
}

View File

@@ -0,0 +1,93 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FieldSet from 'Components/FieldSet';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import styles from './Statistics.css';
import formatBytes from 'Utilities/Number/formatBytes';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import moment from 'moment';
function formatValue(val, formatter) {
if (val === undefined) {
return 'n/a';
}
if (formatter) {
return formatter(val);
}
return val;
}
class Statistics extends Component {
//
// Render
render() {
const {
process,
databaseMain,
databaseLog,
commandsExecuted
} = this.props;
return (
<FieldSet legend="Statistics">
<DescriptionList className={styles.descriptionList}>
<DescriptionListItem
title="Up Time"
data={formatValue(process.startTime, (startTime) => formatTimeSpan(moment().diff(startTime)))}
/>
<DescriptionListItem
title="Processor Time"
data={formatValue(process.totalProcessorTime, formatTimeSpan)}
/>
<DescriptionListItem
title="Memory Working Set"
data={formatValue(process.workingSet, formatBytes)}
/>
<DescriptionListItem
title="Memory Virtual Size"
data={formatValue(process.virtualMemorySize, formatBytes)}
/>
<DescriptionListItem
title="Main Database Size"
data={formatValue(databaseMain.size, formatBytes)}
/>
<DescriptionListItem
title="Logs Database Size"
data={formatValue(databaseLog.size, formatBytes)}
/>
<DescriptionListItem
title="Commands Executed"
data={formatValue(commandsExecuted, formatBytes)}
/>
</DescriptionList>
</FieldSet>
);
}
}
Statistics.propTypes = {
process: PropTypes.object,
databaseMain: PropTypes.object,
databaseLog: PropTypes.object,
commandsExecuted: PropTypes.number
};
Statistics.defaultProps = {
process: {},
databaseMain: {},
databaseLog: {}
};
export default Statistics;

View File

@@ -0,0 +1,46 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import Statistics from './Statistics/Statistics';
class Status extends Component {
//
// Render
render() {
return (
<PageContent title="Diagnostic Status">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Refresh"
iconName={icons.REFRESH}
isSpinning={this.props.isStatusFetching}
onPress={this.props.onRefreshPress}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
<Statistics
{...this.props.status}
/>
</PageContentBody>
</PageContent>
);
}
}
Status.propTypes = {
status: PropTypes.object.isRequired,
isStatusFetching: PropTypes.bool.isRequired,
onRefreshPress: PropTypes.func.isRequired
};
export default Status;

View File

@@ -0,0 +1,59 @@
// @ts-check
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchStatus } from 'Store/Actions/diagnosticActions';
import Status from './Status';
function createMapStateToProps() {
return createSelector(
(state) => state.diagnostic.status,
(status) => {
return {
isStatusFetching: status.isFetching,
status: status.item
};
}
);
}
const mapDispatchToProps = {
fetchStatus
};
class DiagnosticConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.fetchStatus();
}
//
// Listeners
onRefreshPress = () => {
this.props.fetchStatus();
}
//
// Render
render() {
return (
<Status
onRefreshPress={this.onRefreshPress}
{...this.props}
/>
);
}
}
DiagnosticConnector.propTypes = {
status: PropTypes.object.isRequired,
fetchStatus: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DiagnosticConnector);

View File

@@ -35,6 +35,10 @@ function EpisodeQuality(props) {
isCutoffNotMet
} = props;
if (!quality) {
return null;
}
return (
<Label
className={className}

View File

@@ -27,7 +27,7 @@ function EpisodeStatus(props) {
size
} = queueItem;
const progress = (100 - sizeleft / size * 100);
const progress = size ? (100 - sizeleft / size * 100) : 0;
return (
<div className={styles.center}>

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import padNumber from 'Utilities/Number/padNumber';
import styles from './SceneInfo.css';
function SceneInfo(props) {
@@ -60,6 +61,10 @@ function SceneInfo(props) {
key={alternateTitle.title}
>
{alternateTitle.title}
{
alternateTitle.sceneSeasonNumber !== -1 &&
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span>
}
</div>
);
})

View File

@@ -24,6 +24,7 @@ import {
import {
faArrowCircleLeft as fasArrowCircleLeft,
faArrowCircleRight as fasArrowCircleRight,
faAsterisk as fasAsterisk,
faBackward as fasBackward,
faBars as fasBars,
faBolt as fasBolt,
@@ -80,6 +81,7 @@ import {
faSignOutAlt as fasSignOutAlt,
faSitemap as fasSitemap,
faSpinner as fasSpinner,
faStepForward as fasStepForward,
faSort as fasSort,
faSortDown as fasSortDown,
faSortUp as fasSortUp,
@@ -125,6 +127,7 @@ export const CLONE = farClone;
export const COLLAPSE = fasChevronCircleUp;
export const COMPUTER = fasDesktop;
export const DANGER = fasExclamationCircle;
export const DEBUG = fasBug;
export const DELETE = fasTrashAlt;
export const DOWNLOAD = fasDownload;
export const DOWNLOADED = fasDownload;
@@ -138,6 +141,7 @@ export const EXTERNAL_LINK = fasExternalLinkAlt;
export const FATAL = fasTimesCircle;
export const FILE = farFile;
export const FILTER = fasFilter;
export const FOOTNOTE = fasAsterisk;
export const FOLDER = farFolder;
export const FOLDER_OPEN = fasFolderOpen;
export const GROUP = farObjectGroup;
@@ -145,6 +149,7 @@ export const HEALTH = fasMedkit;
export const HEART = fasHeart;
export const HISTORY = fasHistory;
export const HOUSEKEEPING = fasHome;
export const IGNORE = fasTimesCircle;
export const INFO = fasInfoCircle;
export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard;
@@ -177,6 +182,8 @@ export const REORDER = fasBars;
export const RSS = fasRss;
export const SAVE = fasSave;
export const SCHEDULED = farClock;
export const SCRIPT_DEBUG = fasStepForward;
export const SCRIPT_RUN = fasPlay;
export const SCORE = fasUserPlus;
export const SEARCH = fasSearch;
export const SERIES_CONTINUING = fasPlay;

View File

@@ -10,6 +10,7 @@ export const PASSWORD = 'password';
export const PATH = 'path';
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
export const INDEXER_SELECT = 'indexerSelect';
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
export const SELECT = 'select';
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
@@ -30,6 +31,7 @@ export const all = [
PATH,
QUALITY_PROFILE_SELECT,
LANGUAGE_PROFILE_SELECT,
INDEXER_SELECT,
ROOT_FOLDER_SELECT,
SELECT,
SERIES_TYPE_SELECT,

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