1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-23 17:14:18 -04:00

Compare commits

...

322 Commits

Author SHA1 Message Date
Taloth Saldono
243c779ca1 Refactored enum and messages. Also added 9p to the list since mono misclassifies it. 2019-06-25 22:59:54 +02:00
Devin Buhl
7ff6342503 disable wal 2019-06-24 21:55:48 -04:00
Taloth Saldono
1af3e0bd93 Created generic Hinted EnhancedSelectInput components and use it instead of SelectInput 2019-06-20 19:09:28 +02:00
Taloth Saldono
d3662f2302 Added test for turkish FirstCharToLower 2019-06-15 20:38:17 +02:00
Taloth Saldono
5fe34cb593 Fixed: Tag deletion via api if tag is still in use 2019-06-15 20:11:50 +02:00
Taloth Saldono
af5166e95d Fixed: Transmission seeding idle time handling 2019-06-15 19:31:55 +02:00
desimaniac
13907d6711 New: Remove some more retagging groups from filenames. 2019-06-15 10:10:48 -07:00
Mark McDowall
c4c9f0e368 Fixed: Issue searching for series in the UI when tag is removed 2019-06-15 10:09:21 -07:00
Mark McDowall
394932b57f Fixed: Remote path mapping host comparison ignores case
Closes #3169
2019-06-15 09:46:47 -07:00
Mark McDowall
7dff9bc696 Fixed: Errors logged during import when existing episode file is partial removed in the DB
Fixes #3159
2019-06-15 00:48:03 -07:00
Mark McDowall
4713eaffdb Fixed: Mass Editor not showing delete button on narrow screens
Fixes #3142
2019-06-11 23:06:14 -07:00
Mark McDowall
3a7992b1c8 Small UI fixes
Fixed: Season count popover styling
Fixed: URL base of /series linking to the wrong path in some cases
Fixed: Manual import showing error when a different series is selected
Fixed: Error when deleting series from poster list
2019-06-11 23:05:17 -07:00
Mark McDowall
5275aa72fb Clean up FirstCharacterToLower extension + tests 2019-06-10 21:32:42 -07:00
Mark McDowall
9e45b9e808 Fixed: Selecting a release from Interactive Search with an unknown episode 2019-06-10 21:32:42 -07:00
Taloth Saldono
88dfa14046 Wrong escape in help message 2019-06-10 16:30:28 +02:00
Taloth Saldono
dd4216d432 Fixed: Regression preventing empty qbittorrent category
fixes #3158
2019-06-10 12:13:39 +02:00
Taloth Saldono
628ab85de4 New: Configurable Specials folder format 2019-06-10 00:46:46 +02:00
jtpavlock
39ea2dd32f New: Ability to set a post-import label in Deluge, rTorrent, qBittorrent, and uTorrent 2019-06-09 19:54:53 +02:00
Taloth Saldono
1d77c40d0e Support for primary and fallback download client 2019-06-08 15:49:54 +02:00
Taloth Saldono
a3cbb4158c New: Round-robin over available Download Client instead of the first enabled one 2019-05-30 00:38:18 +02:00
Taloth Saldono
52aa84e9f9 Tweaked mediainfo api call to better handle unsupported locales. 2019-05-30 00:32:52 +02:00
lps-rocks
f08fc7493d New: Added downloadId filter to v3 history api for third-party applications
closes #3105
2019-05-30 00:32:17 +02:00
Taloth Saldono
a80b1bbcb3 Added more logging to MediaInfo encoding check for linux. 2019-05-22 19:54:40 +02:00
Carl Downing
7ce8bac3ea Fix grammar and punctuation in DeleteSeriesModalContent 2019-05-18 22:09:19 -07:00
Mark McDowall
5c258797ec Fixed: tag input height not growing in height
Fixes #3124
2019-05-16 22:47:28 -07:00
Taloth Saldono
0dccc7e91e Fixed: Various performance improvements for large collections 2019-05-16 00:22:11 +02:00
Taloth Saldono
9e68653949 Fixed: Slow db migration when upgrading from v2 to v3 with a large collection 2019-05-16 00:07:02 +02:00
Taloth Saldono
83c09b4540 Fixed: Support for SignalR's Server Sent Events transport as an alternative to websockets and long polling 2019-05-16 00:05:15 +02:00
Mark McDowall
5cf2672469 Fixed: Files not replacing a lower quality proper/repack 2019-05-15 14:08:02 -07:00
Mark McDowall
50144721d7 Don't double log exception setting file permissions 2019-05-12 21:53:31 -07:00
Mark McDowall
0fe7da80ab Fixed: Error logged when checking if v2+ anime release is a valid upgrade
Fixes #3114
2019-05-12 16:25:57 -07:00
Mark McDowall
068eb33bf6 New: TVDB ID filter when getting series from API
Closes #2486
2019-05-12 16:14:56 -07:00
Mark McDowall
98b1a7681b Fixed: Monitored status being reset after refresh when series is edited manually 2019-05-12 16:03:31 -07:00
Mark McDowall
488967c6ef Fixed: Parsing of WEB from some file names 2019-05-11 22:20:41 -07:00
Mark McDowall
a5aab95ecf Fixed NZBGet tests take 2 2019-05-09 10:25:09 -07:00
Mark McDowall
345de3654a Fixed NZBGet tests 2019-05-09 09:31:05 -07:00
Mark McDowall
6ea7e785e3 New: Additional information when Sonarr is unable to access a path during import
Closes #1106
2019-05-08 22:56:30 -07:00
Mark McDowall
0c2331f638 Fixed: Ignore deleted duplicates from Nzbget
Fixes #1721
2019-05-08 22:25:10 -07:00
Mark McDowall
5fe1ce1eff Fixed: Don't import duplicate NFO extra files
Fixes #2641
2019-05-08 21:58:50 -07:00
Mark McDowall
6a6d6f9e0d Fixed: Importing of preferred release over a proper/repack 2019-05-08 21:04:12 -07:00
Mark McDowall
30a512c880 Fixed: Episode details on history episode file information 2019-05-08 20:15:21 -07:00
Mark McDowall
2b4519a4ad Fixed: Loading of fonts.css with a URL Base
Fixes #3103
2019-05-08 19:33:47 -07:00
Mark McDowall
3e25d41c0f Another entry into the hall of shame 2019-05-06 08:22:55 -07:00
Mark McDowall
d8baa93289 Only check repacks for revision upgrades 2019-05-05 20:39:52 -07:00
Mark McDowall
d8c2640959 Fixed: Queue tooltips appearing offscreen on mobile devices 2019-05-05 20:03:26 -07:00
Mark McDowall
2ae4337d0d FirstCharToLower 2019-05-05 18:57:33 -07:00
Mark McDowall
0416060643 Tests for repack fix and improve behaviour when release group is unknown 2019-05-05 13:01:16 -07:00
Mark McDowall
2b1fd77ad7 Fixed: Repack check failing for episode file without a known release group 2019-05-05 11:09:48 -07:00
Mark McDowall
43567a3119 Remove old twitter keys 2019-05-05 11:09:48 -07:00
desimaniac
d463c2fbc5 New: Remove 'AsRequested' suffix from release group names 2019-05-04 23:24:13 -07:00
Mark McDowall
940cba5f90 Fixed: Possible issue with manual import of an unknown release 2019-05-04 08:57:59 -07:00
Mark McDowall
7321075631 New: Option to not prefer repacks/propers (for use with Preferred Words)
Closes #3084
2019-05-04 00:33:13 -07:00
Mark McDowall
a06cbc44cd Fixed: Ignore episode title when parsing release group
Fixes #3097
2019-05-03 20:14:35 -07:00
Mark McDowall
088b4af795 Appease stylelint 2019-05-03 18:44:06 -07:00
Mark McDowall
fc0b1e8941 Extra warning for Windows Service issues when prompted to restart after changing host settings
Closes #3094
2019-05-03 17:52:23 -07:00
Mark McDowall
9ad3b12403 Manual Import: Reprocess after selecting series
New: Reprocess changed items when series is changed
Closes #1893
2019-05-03 17:38:06 -07:00
Mark McDowall
88ecec2f9a Fix SAB test 2019-05-03 14:09:19 -07:00
Mark McDowall
4ea5e9ce9b Ignore older episodes in latest season
New: Ignore episodes that aired more than 90 days ago when adding with Latest Season
Closes #826
2019-05-01 23:29:54 -07:00
Mark McDowall
9b617af713 New: Option to opt out of TBA episode title import delays
Closes #3086
2019-05-01 20:36:09 -07:00
Mark McDowall
e70d92f670 New: Restrict repack upgrades to the same release group
Closes #946
2019-04-29 23:38:18 -07:00
Mark McDowall
1b3acb52f1 Fixed: Don't treat NZBs rejected by SABnzbd as successful 2019-04-29 18:18:06 -07:00
Mark McDowall
052ddc11b7 Added a unit test for the NZBGet Final dir fix 2019-04-28 19:27:34 -07:00
Mark McDowall
4e3a5a8823 Only use NZBGet's FinalDir if it's not empty
Fixed: Importing from NZBGet
2019-04-28 19:23:56 -07:00
Mark McDowall
05e17b70b5 New: Show health warning if system time is off expected time
Closes #1422
2019-04-28 11:50:18 -07:00
Mark McDowall
949d764638 Use popper placement for tooltip arrow 2019-04-28 01:08:09 -07:00
Mark McDowall
5c2cb4de80 Improve tooltip performance 2019-04-28 01:08:09 -07:00
Mark McDowall
f68c5cb4f7 QualityDefinition UI fixes 2019-04-28 01:08:08 -07:00
Mark McDowall
06c1f376bc Improve performance of search input selecting series 2019-04-28 01:08:05 -07:00
Mark McDowall
7c7a6a4514 Throw exception if ports are the same at startup 2019-04-28 01:07:56 -07:00
Mark McDowall
5293349785 Limit search input to first character matching when only one character is typed 2019-04-27 20:14:44 -07:00
Mark McDowall
2ee0ae1f9e New: Don't search for unaired anime episodes when searching for season
Closes #2530
2019-04-27 19:13:27 -07:00
Mark McDowall
8143237d25 Re-order PMS settings and rename Kodi connection 2019-04-27 18:55:09 -07:00
Mark McDowall
a426068273 New: Option to use HTTPS with Emby
Closes #2923
2019-04-27 18:54:45 -07:00
Mark McDowall
599b19102e Fixed: Don't allow HTTP and HTTPS to use the same port
Closes #2890
2019-04-27 18:48:23 -07:00
Mark McDowall
e7a979d6b4 Fixed tests after removing sentry logging 2019-04-27 18:44:56 -07:00
Mark McDowall
7991ed0154 New: Reject multi-season releases
Closes #683
2019-04-27 18:27:35 -07:00
Mark McDowall
a9a3e50179 Fixed: Parsing of some anime batches
Closes #2705
2019-04-27 18:27:34 -07:00
Mark McDowall
7642fe046b New: Log when release is matched by ID instead of title
Closes #446
2019-04-27 18:01:45 -07:00
Mark McDowall
ccc2c39d43 Fixed: Cleaning percent signs from release names
Fixes #1998
2019-04-27 16:15:53 -07:00
Mark McDowall
26228e546e Improve error messaging for missing information when searching
Fixed: Show missing absolute episode number/air date error message in interactive search
2019-04-27 16:15:53 -07:00
Mark McDowall
6036bc17c5 New: Use NZBget's FinalDir is set by post-processing script
Closes #2006
2019-04-27 16:15:52 -07:00
Taloth Saldono
ca4b03f48a Fixed typos. 2019-04-26 20:09:12 +02:00
Mark McDowall
b0f59ad988 Removed unused var 2019-04-25 22:22:07 -07:00
Mark McDowall
c9bdf43a0d Use Portal component in AutoSuggestInput 2019-04-25 21:47:29 -07:00
Mark McDowall
dadab50f3b Fixed: Backup path URL
Fixes #3079
2019-04-25 20:30:55 -07:00
Mark McDowall
0b49eba77a Fixed: Root folder selection scrolling
Fixes #3077
2019-04-25 20:24:05 -07:00
Mark McDowall
004b7391c6 Fixed: Math on quality definition limits 2019-04-24 21:46:46 -07:00
Mark McDowall
45c221a3b2 Fixed: Ensure max sized posters aren't returned for some devices 2019-04-24 21:00:15 -07:00
Mark McDowall
364f074be1 Fixed: Removed nzbs.org Newznab preset 2019-04-24 20:39:45 -07:00
Qstick
e82eded1e9 Fixed: Support new feed url format IPTorrents
Fixes #3071
2019-04-24 00:12:16 -07:00
Mark McDowall
b298f84f51 Fixed: Parsing of first aired date on Arabic systems 2019-04-24 00:12:16 -07:00
Mark McDowall
26ff28aae6 New: Tooltips for quality size limits 2019-04-24 00:12:16 -07:00
netpok
588eb6f691 New: Detect mergerfs mounts 2019-04-22 20:28:11 -07:00
Qstick
c10448af0b Fixed: Roksbox SeriesImages can lead to NullRef 2019-04-22 20:27:07 -07:00
Mark McDowall
0eb7973ab0 New: Show tooltips for changeable columns on Manual Import
Closes #3069
2019-04-22 20:19:20 -07:00
Taloth Saldono
150a87f2ea Fix VideoFileInfoReader tests after mediainfo update... take 2 2019-04-21 00:31:27 +02:00
Taloth Saldono
2505a19a88 Fixed: Air-time adjustment for Amazon/Hulu releasing 4+ episodes on one day 2019-04-21 00:06:13 +02:00
Mark McDowall
beea02cea9 Fixed: Don't reject import with missing episode title if renaming is off 2019-04-17 22:46:58 -07:00
Mark McDowall
600b5cfa8e Menu separator shown when scrollbar is visible 2019-04-17 22:43:35 -07:00
Mark McDowall
8055b5e5da New: Output Path column in Queue
Closes #3058
2019-04-17 22:39:37 -07:00
Mark McDowall
4933a75d15 Fixed: Don't include year 0 in series folder name
Fixes #3057
2019-04-17 22:25:41 -07:00
Mark McDowall
478e13b0fd Replace react-tether with react-popper 2019-04-17 02:21:58 -07:00
Mark McDowall
9c26da70da Fix VideoFileInfoReader tests after MediaInfo upgrade 2019-04-17 02:20:26 -07:00
Mark McDowall
872a8d983b Fixed: Manual import of unknown series items in Activity: Queue 2019-04-17 02:20:24 -07:00
Mark McDowall
47d3fe1de5 Fixed: Manage episode files for season text 2019-04-13 23:29:39 -07:00
Mark McDowall
9421af2c3f Backup directory is a path 2019-04-13 23:28:47 -07:00
Stephan Renggli
90626e353f New: Gotify notifications
Closes #3033
2019-04-13 15:50:22 -07:00
Mark McDowall
b3e019e7a0 New: Sort preferred words by score when displaying in the UI 2019-04-13 12:56:17 -07:00
Mark McDowall
84a0a0743b New: Upgrade MediaInfo to 18.12 (macOS and Windows) 2019-04-13 11:32:18 -07:00
Mark McDowall
377bd6e2b7 Collapse calendar view buttons on narrower screens (<= 1200px) 2019-04-13 11:08:59 -07:00
Mark McDowall
dc42c6a1df Fixed: Refresh on series list spinning forever in some cases 2019-04-12 23:33:51 -07:00
Mark McDowall
0ca70a3197 Fixed: Long path support on Windows 2019-04-12 19:21:49 -07:00
Mark McDowall
889f92268e Fixed: Series footer shows statistics based on filtered series list 2019-04-12 19:05:23 -07:00
morpheus133
24fba7a36d New: Use IMDB ID when searching supported indexers 2019-04-12 18:02:08 -07:00
Mark McDowall
c90672a5ab More renaming tokens
New: Always show Air Date renaming token in help modal
Fixed: Added missing media info renaming tokens
2019-04-12 17:48:45 -07:00
Mark McDowall
81c8fc0381 Fixed: Improve exception logging when unable to connect to Plex Media Server 2019-04-12 17:46:47 -07:00
Mark McDowall
7922a3856e updateMechanism isn't available while fetching 2019-04-11 23:56:58 -07:00
Mark McDowall
9aea452829 History details for unknown event type 2019-04-11 23:56:37 -07:00
Mark McDowall
e797b759b7 Fixed: Use Download Client name for grabbed history events 2019-04-11 23:43:41 -07:00
Mark McDowall
b60d5f837e Remove DownloadProtocol from v3 ReleaseResource 2019-04-11 23:41:06 -07:00
Mark McDowall
877235c8a2 Fixed: Already Imported check failing for some torrent releases 2019-04-11 17:49:09 -07:00
Mark McDowall
558bbeaa34 Fixed: SSL Certificate validation when port is used
Closes #3039
2019-04-11 09:00:59 -07:00
Mark McDowall
0911abcfc0 Improve certificate validation registration
Fixed: Certificate validation during startup
Fixed: Errors removing Windows service

Closes #3037
Closes #3038
2019-04-10 19:34:37 -07:00
Mark McDowall
04850331ce Fix long path support overrides in mono 2019-04-10 00:14:26 -07:00
Mark McDowall
b58336f4c8 Fix case of LegendIconItem.css 2019-04-10 00:13:56 -07:00
Mark McDowall
4665b4fb37 New: Target .net 4.6.2 2019-04-09 22:02:43 -07:00
Mark McDowall
49197a2ae0 Update executable icon 2019-04-09 21:54:44 -07:00
Mark McDowall
c718db21da Fixed: Certificate validation for local IP addresses instead of hostnames 2019-04-09 20:57:12 -07:00
Mark McDowall
35f28bdc71 Update docs
- Fix project names
- Unify dev environment setup
2019-04-09 20:47:50 -07:00
Mark McDowall
bf86b09f4e Fixed: Interactive search grabs rejected due to validation 2019-04-09 09:17:14 -07:00
Mark McDowall
cdde8cfdbe Protocol instead of download protocol 2019-04-08 22:47:56 -07:00
Mark McDowall
cd26b8f728 DownloadProtocol is an integer for release/push 2019-04-08 17:51:54 -07:00
Mark McDowall
8abfc7609e Send downloadProtocol in release/push integration test 2019-04-07 22:15:12 -07:00
Mark McDowall
7d06e5d684 HTTPS certificate validation options
New: Enable HTTPS certificate validation by default
New: Option to disable certificate validation for all or only local addresses
2019-04-06 23:43:47 -07:00
Mark McDowall
439870a546 Don't render table options modal content when it's closed 2019-04-06 23:22:52 -07:00
Mark McDowall
82b35f095e Improve series index performance during series refresh 2019-04-06 23:22:52 -07:00
Mark McDowall
b40d7d89a1 Improve selectors in PageConnector to reduce re-rendering 2019-04-06 23:22:52 -07:00
Mark McDowall
8f3dbbc356 Fixed: Return better error message if username or password is null for forms login 2019-04-06 21:33:48 -07:00
Mark McDowall
f748891b4b Fixed: Ensure loading message doesn't change on re-render 2019-04-06 19:22:24 -07:00
Mark McDowall
e325a5c27e Better response if invalid JSON is received through the API 2019-04-06 19:21:59 -07:00
Mark McDowall
7d131fbb3d ReleasePushModule uses ReadResourceFromRequest 2019-04-05 20:23:59 -07:00
Mark McDowall
2b63110323 Series index selector improvements 2019-04-05 20:17:24 -07:00
Mark McDowall
a63ca0f0e9 More file browsing improvements 2019-04-05 20:16:57 -07:00
Mark McDowall
5b2d0a2ade Fixed: Store columns for History table between refreshes 2019-04-05 17:53:11 -07:00
Mark McDowall
1a984df11a Fixed: Error on calendar with unknown items in the queue 2019-04-05 17:52:02 -07:00
Mark McDowall
fdc7a19628 Fixed: Error displayed occasionally after removing series from the series list
Fixes #3018
2019-04-04 19:35:06 -07:00
Mark McDowall
8087996c8e Better selection of executing commands in series list 2019-04-04 18:45:22 -07:00
Mark McDowall
e5f264a510 PahtInputConnector default prop for includeFiles 2019-04-04 18:45:21 -07:00
Mark McDowall
0a2eb74c26 Release module validation in v3 2019-04-04 18:45:20 -07:00
Mark McDowall
d5bb3bd799 Fixed: Centering of expand/collapse icon for season 2019-04-01 17:26:31 -07:00
addisonbabcock
cfa9d88be6 Fixed quality typo on manual import 2019-03-28 20:06:11 -07:00
Taloth Saldono
ac4617ae51 Fixed eslint error. 2019-03-26 19:11:51 +01:00
Taloth Saldono
f6bcadfeec Merge branch 'develop' into phantom-develop 2019-03-26 19:07:26 +01:00
Mark McDowall
5272078ea3 Fixed: Sorting by age when releases are less than a day old
Fixes #3012
2019-03-25 23:33:00 -07:00
Mark McDowall
b6257400ec Fixed: Sorting of search results in series search box
Closes #3013
2019-03-25 23:33:00 -07:00
Mark McDowall
c3a6e01040 Fix file browser when files should be included 2019-03-25 23:33:00 -07:00
Mark McDowall
b63cbbdaaa New: Release title column in queue 2019-03-25 23:33:00 -07:00
desimaniac
81d3f35034 Remove WhiteRev and BUYMORE suffixes from release group names 2019-03-24 19:18:58 +01:00
Taloth Saldono
1fc2866032 Fixed: Include all download items if no category is specified in rtorrent.
closes #3002
2019-03-24 15:27:41 +01:00
Taloth Saldono
eb2e7b9c79 Continue Test in case of validation warnings. 2019-03-24 15:22:50 +01:00
Taloth Saldono
cab900f656 Don't skip magnet links with included trackers if dht is disabled. 2019-03-24 14:58:13 +01:00
Taloth Saldono
e2b91e5dc4 Fixed: Detecting if qbittorrent seeding time limit has been reached 2019-03-23 22:58:43 +01:00
Mark McDowall
00a66e9030 Fixed: Parsing of some WEB releases
Fixes #3001
2019-03-23 11:56:17 -07:00
Mark McDowall
775c8780a6 Fixed: Consistent icon position for toolbar buttons 2019-03-23 11:56:17 -07:00
Mark McDowall
50a968ace8 Renamed Manual Import on series details page 2019-03-23 11:56:16 -07:00
Mark McDowall
db41104d9b Fixed: Queue count badge showing warning/error incorrectly 2019-03-23 11:56:16 -07:00
Mark McDowall
95bb73c5ac Eliminate gulp-flatten 2019-03-23 11:56:16 -07:00
Alan Yee
7ed2776476 Updated links in README.md 2019-03-23 11:14:29 -07:00
Taloth Saldono
341dfb934d Fixed: Typo in ical url handling when choosing premieres only. 2019-03-17 23:23:23 +01:00
hatharry
ecebe73c33 Fixed: Emby library update
Fixes #2662
2019-03-16 15:56:33 -07:00
Mark McDowall
1b0c6b919f Update readme with new requirements for v3 2019-03-16 15:25:03 -07:00
Mark McDowall
06b876cf8b On Download to On Import on card 2019-03-16 14:53:30 -07:00
Mark McDowall
de0d0a3526 New: Discord Notifications
Closes #1511
2019-03-16 14:51:12 -07:00
Mark McDowall
7f99ac0efa Added discord link to UI 2019-03-15 18:02:27 -07:00
Mark McDowall
d2980c58ec Fixed: MediaInfo AudioCodec token helper in UI 2019-03-15 18:02:27 -07:00
Taloth Saldono
d244ed6c64 More descriptive message if indexer connection test was successful but yielded no results. 2019-03-14 21:01:13 +01:00
Mark McDowall
a30d9a1af2 Fixed: Plex authentication 2019-03-14 00:53:53 -07:00
Mark McDowall
7acd6a4d3c Can't login with a username and a blank password 2019-03-14 00:53:53 -07:00
desimaniac
a6fdcb7493 Parser: Removes any combination of 'rakuv*` from release group names. 2019-03-12 20:05:54 +01:00
Taloth Saldono
0cc2437a9d Fixed typo in XDG_CONFIG_HOME handling.
closes #2989
2019-03-12 08:57:30 +01:00
Taloth Saldono
8102cb63ae Fixed: Interactive Search for Specials on BTN 2019-03-10 14:44:18 +01:00
Taloth Saldono
5062d74041 Reverted in-memory signalr keypair in favor of a .config directory. 2019-03-10 13:16:27 +01:00
Taloth Saldono
feebb349d5 Linting error. 2019-03-09 00:21:06 +01:00
Taloth Saldono
1036813b97 Fixed: Finetuned color-impaired mode styling in Calendar. 2019-03-08 23:48:45 +01:00
Taloth Saldono
6b405700ec Make sure something appears in the trace file before trying to read it. 2019-03-08 20:58:54 +01:00
Taloth Saldono
29b4a83d93 Wait for commands to finish between tests. 2019-03-08 20:29:38 +01:00
Taloth Saldono
7b159c1e63 Removed Nyaa Integration tests and increased logging detail during integration tests. 2019-03-08 19:37:59 +01:00
Taloth Saldono
02c64ad3a5 Fixed: Not being able to use MediaInfo VideoDynamicRange token to renaming options. 2019-03-08 18:37:10 +01:00
Taloth Saldono
13c625d7c0 Fixd test on mono. 2019-03-06 23:19:13 +01:00
Taloth Saldono
9a3f49bf9c Merge branch 'develop' into phantom-develop
# Conflicts:
#	src/NzbDrone.Common.Test/Http/HttpClientFixture.cs
#	src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs
2019-03-06 22:36:23 +01:00
Taloth Saldono
6698ca400c Handle special mount filtering at a higher level.
closes #2743
2019-03-06 22:34:17 +01:00
Mark McDowall
6bb649bac5 autoprefixer and webpack use the same browser list config 2019-03-05 20:01:21 -08:00
Mark McDowall
b72b74b6c6 Upgraded react and react-dom packages 2019-03-05 19:58:58 -08:00
Mark McDowall
e7bfea8c69 Update react-tether package 2019-03-05 19:38:39 -08:00
Mark McDowall
de7e805718 Limit replacement of colons 2019-03-05 18:08:34 -08:00
Mark McDowall
7b5e8646eb Transpile UI for old browsers 2019-03-05 18:07:51 -08:00
Mark McDowall
966e147a20 Replaced gulp-util with ansi-colors, updated packages 2019-03-05 00:32:21 -08:00
Mark McDowall
269a5bd914 New: Log conflicting TVDB ID when unknown series is an alias for another series 2019-03-05 00:30:37 -08:00
Mark McDowall
4f1f56f653 Another broken test 2019-03-04 21:06:32 -08:00
Mark McDowall
6b517d7ffd Fix broken tests 2019-03-04 19:47:24 -08:00
Mark McDowall
4eb9a1cfa5 New: Replace colon with space and dash instead of just dash
Closes #2961
2019-03-04 19:12:00 -08:00
Mark McDowall
db3aeb9ab5 Removed unused babel-plugin-transform-react-jsx-source 2019-03-04 19:00:22 -08:00
Mark McDowall
984b7bbeea Fixed: All preferred words being added to filename 2019-03-04 18:31:53 -08:00
Mark McDowall
582beed977 Fixed: Fonts not loading on reload 2019-03-04 18:31:19 -08:00
Mark McDowall
7fa8bd5613 Appease stylelint 2019-03-03 21:16:36 -08:00
Mark McDowall
4474172c40 Fix index.css 2019-03-03 21:14:21 -08:00
Mark McDowall
1dde397e2b Upgrade del 2019-03-03 20:58:53 -08:00
Mark McDowall
7ea3b6ca15 Upgraded to gulp 4
Use `yarn watch` to use local gulp
2019-03-03 20:56:16 -08:00
Taloth Saldono
e52fcf843c Handle Deluge v2 beta breaking change in their api.
closes #2412
2019-03-03 23:10:28 +01:00
Taloth Saldono
08ba273089 fixed qbittorrent tests failing due to incorrect test setup. And http tests failed due to httpbin changing their output. 2019-03-03 21:19:25 +01:00
Mark McDowall
956e7b564c Upgrade eslint and stylelint-order... again 2019-03-03 11:39:17 -08:00
Mark McDowall
08990dd58a Upgraded to webpack 4 2019-03-03 11:29:12 -08:00
Taloth Saldono
faa2d632e5 New: Indexer Seed Limit settings applied to new downloads for qBittorrent
closes #2607
2019-03-03 20:25:31 +01:00
Taloth Saldono
1b939ebf4b Fixed: Magnet Link progress visualisation and adding magnet links if dht is disabled in qBittorrent 2019-03-03 19:29:25 +01:00
Mark Bebbington
aa46216117 Fixed: qBittorrent api v2 support (qbit v4.1+)
fixes #2887
closes #2951
ref #2945
2019-03-03 19:26:50 +01:00
Mark McDowall
ca32434535 Update redux, reselect and moved to connected-react-router 2019-02-28 20:01:06 -08:00
Mark McDowall
ebf4f17f17 Fix route to series details from search input 2019-02-28 20:00:08 -08:00
Mark McDowall
22778091f9 RelativeDateCell PureComponent 2019-02-28 18:39:12 -08:00
Mark McDowall
4d389ae5ce Upgrade prop-types package 2019-02-27 21:37:49 -08:00
Mark McDowall
39e3120058 Fix page jump bar not rendering/rendering in the wrong order 2019-02-27 21:00:35 -08:00
Mark McDowall
6b15d7e260 Fix casing of RegexTermMatcher 2019-02-27 21:00:35 -08:00
Mark McDowall
277025775d Upgraded most react packages 2019-02-27 20:50:19 -08:00
Mark McDowall
dbd649bfb5 Upgraded sentry, clipboard, filesize, moment, normalize.css and qs packages 2019-02-27 18:54:25 -08:00
Mark McDowall
df4ddba1ab Missing root folder health check icon 2019-02-27 18:42:58 -08:00
Mark McDowall
21985d0814 Upgrade linting and CSS packages 2019-02-27 18:38:56 -08:00
Mark McDowall
f7031dcb7f Upgrade fontawesome packages 2019-02-27 18:33:09 -08:00
Mark McDowall
0219e62979 Use fuse.js for series searching in UI
Closes #2954
2019-02-27 17:52:05 -08:00
Mark McDowall
e66725047a Updated yarn packages for node 10 support 2019-02-27 17:13:27 -08:00
Mark McDowall
84fa99a126 Icon, SeriesIndexFooter -> PureComponent 2019-02-26 19:46:18 -08:00
Mark McDowall
78b3c9552b Fixed: Include matching value of preferred word regex, not the actual regex 2019-02-26 19:06:05 -08:00
Mark McDowall
1222aeaab6 Fixed: Select all in Episode File editor 2019-02-26 19:06:05 -08:00
Mark McDowall
cbbfc5b58c Remove logging of Unable to format audio channels using 'AudioChannels' due to old schema 2019-02-26 19:06:05 -08:00
Mark McDowall
11164ab838 New: Bulk select language and quality in Manual Import 2019-02-26 19:06:05 -08:00
Mark McDowall
8339f7fdb3 Fixed: Don't add TV Maze ID to format if unknown 2019-02-26 19:06:05 -08:00
Taloth Saldono
220cd84ef5 Fixed: SignalR requiring a home directory to function properly. 2019-02-23 21:18:52 +01:00
Wu Haotian
e5fa446159 Downgrade event-stream
https://github.com/dominictarr/event-stream/issues/116
2019-02-22 16:27:47 -08:00
Matt Evans
70c320e98b New: Added {MediaInfo VideoDynamicRange} renaming token to include HDR in the filename 2019-02-22 19:45:02 +01:00
Wu Haotian
8e486da928 New: Added parser support for common Chinese release formats 2019-02-20 20:04:05 +01:00
Wu Haotian
7ae906863d Fix filename in PostBuildEvent
NzbDrone.{Mono, Windows} has been renamed to Sonarr.{Mono, Windows} in adaf428aa7
2019-02-20 20:04:05 +01:00
Mark McDowall
fb4483fdcf Switch to https for httpbin URL test 2019-02-20 00:31:39 -08:00
Mark McDowall
e32e6e0bec Groups must contain multiple qualities 2019-02-19 18:48:54 -08:00
Mark McDowall
05e7b90aab Fixed: Correct rejection message when profile does not allow upgrades
Fixes #2958
2019-02-19 18:30:57 -08:00
Mark McDowall
ee59f91ba2 Pending releases have languages too 2019-02-19 18:27:48 -08:00
Mark McDowall
ece3241041 Fixed: Adding series with unknown IMDB ID and series folder includes IMDB ID 2019-02-19 18:27:26 -08:00
Mark McDowall
fcb1bcb91b New: Setting monitor to None when adding series will unmonitor the series as well 2019-02-19 18:26:34 -08:00
Mark McDowall
728c0e8272 Fixed: Select all on Activity: Queue 2019-02-19 17:54:57 -08:00
Mark McDowall
9b212d11f0 Fixed: Error when editing torrent indexer 2019-02-14 09:14:16 -08:00
Taloth Saldono
223209e1eb Tweaked language parser since PR isn't merged yet. 2019-02-12 21:59:26 +01:00
Taloth Saldono
5b741a10db Fixed: Season pack with Special in series title was treated as unknown special 2019-02-12 21:56:55 +01:00
Matt Evans
1606ea19a8 New: Added support for DTS-HD MA and TrueHD Atmos in MediaInfo AudioCodec. 2019-02-11 07:50:50 +11:00
Taloth Saldono
e5632019db Simplified more RegexReplace instances. 2019-02-09 21:13:13 +01:00
Taloth Saldono
ff994d594a Fixed error in unicode cleanup code removing most non-latin characters instead of just invalid ones. 2019-02-09 21:13:13 +01:00
Taloth Saldono
0214ced8f0 New: Added Icelandic language and improved Chinese language detection 2019-02-09 20:57:22 +01:00
Mark McDowall
813e5e1db8 New: Sort queue by status 2019-02-07 18:48:51 -08:00
Mark McDowall
69627911b3 New: Highlight currently installed version on System: Updates 2019-02-07 18:44:22 -08:00
Mark McDowall
29f905b942 Root folder handler for signalR 2019-02-07 18:43:56 -08:00
Mark McDowall
551fa7fe10 Fixed: Banner not growing when most columns are hidden 2019-02-07 18:43:43 -08:00
Mark McDowall
2ce3cd4c2a Fixed: Improve readability of text on light blue labels 2019-02-07 18:42:54 -08:00
Mark McDowall
8ea24a6b09 Fix QualityModelComparer test when respecting group order 2019-02-07 09:25:42 -08:00
Taloth Saldono
00ca91399a ESlint error 2019-02-07 13:33:08 +01:00
Taloth Saldono
cf327077e9 Fixed: Regression in folder move logic preventing updater from working. 2019-02-07 12:57:45 +01:00
Taloth Saldono
f215ba9bac Fixed: Additional reverse title parser patterns.
fixes #2943
2019-02-07 12:57:45 +01:00
Mark McDowall
c3c6b3d166 Fixed: Importing completed downloads from NZBGet with post processing script failing
Fixes #2919

# Conflicts:
#	src/NzbDrone.Core.Test/MediaFiles/ImportApprovedEpisodesFixture.cs
2019-02-06 19:36:37 -08:00
Mark McDowall
b319cb9525 Fixed: Importing completed downloads from NZBGet with post processing script failing
Fixes #2919
2019-02-06 19:30:02 -08:00
Mark McDowall
95cd327022 Already is spelled already 2019-02-06 19:30:01 -08:00
Mark McDowall
807cfebf76 Number input and max release size limit increased
Fixed: Number input changing value while typing
New: Maximum size limit has been doubled

Closes #2921
2019-02-06 19:30:01 -08:00
Mark McDowall
a29b1259b5 Sort search input by sortTitle 2019-02-06 19:30:00 -08:00
Mark McDowall
102b8afd66 Various UI fixes
Fixed: Manual Import not working if no episodes are Missing
Fixed: Allow spaces in Must/Must not contain fields
Fixed: Show loading indicator when managing episodes and fetching data from the server
Fixed: Series images not loading for all search results
New: Auto focus search input when navigating to Add Series page
2019-02-06 19:30:00 -08:00
Mark McDowall
561fdef815 Fixed: Don't use extended episode number as release group 2019-02-06 19:30:00 -08:00
Mark McDowall
47db1db861 Minimize data sent when adding a new Indexer, Download Client, etc 2019-02-06 19:30:00 -08:00
Mark McDowall
de3d7e925a Updated yarn.lock with postcss-color-function 2019-02-06 19:29:59 -08:00
Mark McDowall
b04c286efc Fixed: Settings changes being cleared when leaving page despite confirmation they would be 2019-02-06 19:29:59 -08:00
Mark McDowall
b8b82189f7 Fixed: Quality Profile group order no longer used when ordering results 2019-02-06 19:29:59 -08:00
Mark McDowall
08b65a954d Fixed: Cutoff unmet episode search failing when there are unknown items in the queue 2019-02-06 19:29:59 -08:00
Mark McDowall
c6c998dc9f Fixed: poster not showing when adding a new series on a larger screen 2019-02-06 19:29:59 -08:00
Taloth Saldono
b3ff91608e Fixed: Ignore series title before SxxExx when parsing language.
ref #861
2019-02-04 22:01:25 +01:00
Taloth Saldono
1d862db7c9 Fixed: Korean shows with more than 2 digit episode numbers.
closes #2901
2019-02-04 21:39:28 +01:00
Taloth Saldono
070cbeebbe Allow -suffix in PackageVersion and added that and PackageAuthor to the About page. 2019-02-04 20:44:47 +01:00
Mark McDowall
2c95f07cb2 Another path test fix 2019-02-01 10:58:02 -08:00
Mark McDowall
4a2277b424 Fix path tests 2019-02-01 10:57:49 -08:00
Mark McDowall
a1f02916d4 Fixed: Importing of completed download when not a child of the download client output path 2019-02-01 10:57:39 -08:00
Mark McDowall
900dfd92d0 Fixed: Getting parent of UNC paths 2019-02-01 10:56:48 -08:00
Mark McDowall
d6997b0588 Fixed getting parent path from a path without another slash
Fixed: Manual Import failing for some paths
2019-02-01 10:55:28 -08:00
Taloth Saldono
86c74b3ee0 Fixed failing ConfigFileProvider tests due to ConsoleLogLevel property error. 2019-01-21 22:28:45 +01:00
Taloth Saldono
b1a8c70d20 Moved fast MoveSeriesFolder logic if same RootFolder into DiskTransferService. 2019-01-21 22:18:37 +01:00
Taloth Saldono
9107d1678c Fixed: Failure to match S12E00 special due to episode file vs folder being parsed differently. 2019-01-21 21:24:27 +01:00
Taloth Saldono
095234babc Added Console log level option in configfile, which defaults to Info. 2019-01-21 21:24:27 +01:00
Mark McDowall
8aecec507e New: Ability to forcibly grab a release from Interactive Search
Closes #395
2019-01-18 16:35:19 -08:00
Mark McDowall
70fb1551af New: Log when media info is unavailable for a file when building a file name 2019-01-18 16:35:19 -08:00
Mark McDowall
fb67e123f4 Fixed: Changing series view 2019-01-18 16:35:19 -08:00
Mark McDowall
ae2efbc116 Fixed: QueueSpecification failing when an unknown item is in the queue 2019-01-18 16:35:19 -08:00
Mark McDowall
4bc0ffa74d Improve renaming of series folder within the same root folder 2019-01-18 16:35:19 -08:00
Taloth Saldono
6fed932a61 Tweaked Color-Impaired styling for Series Index. 2019-01-18 23:50:08 +01:00
Taloth Saldono
939ebcf897 Added missing references to test projects. 2019-01-12 13:40:28 +01:00
Taloth Saldono
1239fa874d Merge branch 'develop' into phantom-develop
# Conflicts:
#	src/NzbDrone.Api/Extensions/Pipelines/GZipPipeline.cs
#              Merged changes to src/Sonarr.Http/Extensions/Pipelines/GZipPipeline.cs
2019-01-12 13:32:11 +01:00
Taloth Saldono
779ab39f50 Fixed failing test 2019-01-12 13:30:08 +01:00
Taloth Saldono
00283e3d6e New: Limit indexer/download client backoff to 5 min during the first 15 min of application start.
closes #2366
2019-01-12 13:15:41 +01:00
Taloth Saldono
2b4429f8b7 Fixed: Erroneously matching Anime 10.5 special as 10.
fixes #2868
2019-01-12 13:14:47 +01:00
Taloth Saldono
2446c4185a Added 10-bit to parser cleanup.
fixes #2870
2019-01-12 13:14:47 +01:00
Taloth Saldono
04900e5f90 Tweaked reverse title detection to handle triple digit episode numbers.
fixes #2871
2019-01-12 13:14:47 +01:00
Mark McDowall
8abdb8bf51 Update test for disabling cache 2019-01-11 11:55:41 -08:00
Mark McDowall
e217068dbd Another path test fix 2019-01-10 23:26:25 -08:00
Mark McDowall
e3a9f753d2 Fix path tests 2019-01-10 20:32:41 -08:00
Mark McDowall
fc2a586453 Set max-age=0 on resources that should not be cached 2019-01-10 20:32:41 -08:00
Mark McDowall
979ea449bd Fixed: Edit button for Remote Path Mapping hidden on small screens 2019-01-10 20:32:41 -08:00
Mark McDowall
ba5e2cfc45 Series type filter/sort
New: Filter/sort by series type
Fixed: Filtering excluding multiple values (is not x or y)
2019-01-10 20:32:41 -08:00
Mark McDowall
3b565d8bb1 New: Table options in page toolbar in addition to table header 2019-01-10 20:32:41 -08:00
Mark McDowall
21a92b62fd Fixed: Various issues with unknown items in queue 2019-01-10 20:32:40 -08:00
Mark McDowall
7e33261ccc Fixed: Move series logging a failure and a success message 2019-01-10 18:15:43 -08:00
Mark McDowall
6a489a0b8f Fixed: Importing of completed download when not a child of the download client output path 2019-01-10 18:15:42 -08:00
Mark McDowall
44e9c77568 UI styling/propType fixes 2019-01-10 18:15:42 -08:00
Mark McDowall
77816aebac Fixed: Series index table header when banners are shown 2019-01-10 18:15:41 -08:00
Mark McDowall
9dd967f2aa Fixed: Getting parent of UNC paths 2019-01-10 18:15:41 -08:00
Mark McDowall
ef7a08879f New: Alternate styling for progress bars when color impaired mode is enabled 2019-01-10 18:15:40 -08:00
Mark McDowall
647e444a07 New: Add root folder to media management settings 2019-01-10 18:15:40 -08:00
Mark McDowall
c417239652 Fixed: Don't auto zoom when focusing inputs on mobile devices, namely iOS 2019-01-10 18:15:39 -08:00
Mark McDowall
e10c92878d Fixed: Calendar error after queue is refreshed 2019-01-10 18:15:39 -08:00
Mark McDowall
fc376bfe3f Fixed: Validation failures not being shown if adding a series fails 2019-01-10 18:15:34 -08:00
Mark McDowall
36fe4eaa49 Fixed: Log events not loading from the first page when revisiting 2019-01-10 18:13:56 -08:00
Mark McDowall
a3baab9671 Fixed: Failing to search for recently added series when there are unknown items in the queue 2019-01-10 18:13:55 -08:00
Mark McDowall
edd6c0bd4c Fixed getting parent path from a path without another slash
Fixed: Manual Import failing for some paths
2019-01-10 18:13:54 -08:00
Taloth Saldono
ce59db528b Fixed: Mono bug causing memory leakage when http connections use gzip compression.
The bug is registered upstream, but this commit works around the problem by doing the gzip decompression separately from the http stack.

Ref #2296
2019-01-10 20:13:48 +01:00
780 changed files with 16694 additions and 8513 deletions

View File

@@ -9,7 +9,7 @@ insert_final_newline = true
indent_style = space
indent_size = 4
[*.{js,html,js,hbs,less}]
[*.{js,html,js,hbs,less,css}]
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

1
.npmrc
View File

@@ -1 +0,0 @@
save-prefix=""

View File

@@ -1 +1,2 @@
save-prefix ""
save-exact true
registry "https://registry.yarnpkg.com"

View File

@@ -7,21 +7,7 @@ Setup guides, FAQ, the more information we have on the wiki the better.
## Development ##
### Tools required ###
- Visual Studio 2015
- HTML/Javascript editor of choice (Sublime Text/Webstorm/Atom/etc)
- npm (node package manager)
- git
### Getting started ###
1. Fork Sonarr
2. Clone (develop branch) *you may need pull in submodules separately if you client doesn't clone them automatically (CurlSharp)*
3. Run `npm install`
4. Run `npm start` - Used to compile the UI components and copy them.
Leave this window open.
If you have gulp globally installed you can use `gulp watch` instead
5. Compile in Visual Studio
See the readme for information on setting up your development environment.
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Sonarr/Sonarr/issues "Github Issues") so work is not duplicated (If you want to add something not already on there, please talk to us first)

View File

@@ -20,33 +20,39 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
### Requirements
* Visual Studio 2015 (https://www.visualstudio.com/vs/)
* [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/articles/working-with-repositories)
* 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 `npm install`
* Start gulp to monitor your dev environment for any changes that need post processing using `npm start` command.
* Install the required Node Packages `yarn`
*Please note gulp must be running at all times while you are working with Sonarr client source files.*
### Backend Development
### Development
* Open `NzbDrone.sln` in Visual Studio
* 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
### UI Development
* Run `yarn watch` to build UI and rebuild automatically when changes are detected
* Run Sonarr.Console.exe (or debug in Visual Studio)
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2017
* Copyright 2010-2019
### Sponsors
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
* [ReSharper](http://www.jetbrains.com/resharper/)
* [WebStorm](http://www.jetbrains.com/webstorm/)
* [TeamCity](http://www.jetbrains.com/teamcity/)

View File

@@ -28,6 +28,12 @@
"react"
],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"filenames/match-exported": ["error"],
@@ -209,7 +215,6 @@
"lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }],
"max-depth": ["error", {"maximum": 5}],
"max-nested-callbacks": ["error", 4],
"max-params": ["error", 6],
"max-statements": "off",
"max-statements-per-line": ["error", { "max": 1 }],
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred", "DragDropContext", "DragLayer", "DragSource", "DropTarget"]}],

View File

@@ -24,7 +24,7 @@
"ignoreAtRules": [
"/^add\\-mixin$/",
"/^define\\-mixin$/"
]
]
}
],
"at-rule-no-vendor-prefix": true,

35
frontend/babel.config.js Normal file
View File

@@ -0,0 +1,35 @@
const loose = true;
module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {
development: {
presets: [
['@babel/preset-react', { development: true }]
],
plugins: [
'babel-plugin-inline-classnames'
]
},
production: {
presets: [
'@babel/preset-react'
],
plugins: [
'babel-plugin-transform-react-remove-prop-types'
]
}
}
};

6
frontend/browsers.js Normal file
View File

@@ -0,0 +1,6 @@
module.exports = [
'>0.25%',
'not ie 11',
'not op_mini all',
'not chrome < 60'
];

View File

@@ -1,15 +1,18 @@
const gulp = require('gulp');
const runSequence = require('run-sequence');
require('./clean');
require('./copy');
require('./webpack');
gulp.task('build',
gulp.series('clean',
gulp.parallel(
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
)
)
);
gulp.task('build', () => {
return runSequence('clean', [
'webpack',
'copyHtml',
'copyFonts',
'copyImages',
'copyJs'
]);
});

View File

@@ -1,15 +1,15 @@
var path = require('path');
var gulp = require('gulp');
var print = require('gulp-print').default;
var cache = require('gulp-cached');
var livereload = require('gulp-livereload');
var paths = require('./helpers/paths.js');
const path = require('path');
const gulp = require('gulp');
const print = require('gulp-print').default;
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))
@@ -17,7 +17,7 @@ gulp.task('copyJs', () => {
});
gulp.task('copyHtml', () => {
return gulp.src(paths.src.html)
return gulp.src(paths.src.html, { base: paths.src.root })
.pipe(cache('copyHtml'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
@@ -26,20 +26,20 @@ gulp.task('copyHtml', () => {
gulp.task('copyFonts', () => {
return gulp.src(
path.join(paths.src.fonts, '**', '*.*')
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyFonts'))
.pipe(print())
.pipe(gulp.dest(paths.dest.fonts))
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyImages', () => {
return gulp.src(
path.join(paths.src.images, '**', '*.*')
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyImages'))
.pipe(print())
.pipe(gulp.dest(paths.dest.images))
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});

View File

@@ -1,8 +1,5 @@
require('./build.js');
require('./clean.js');
require('./copy.js');
require('./imageMin.js');
require('./start.js');
require('./stripBom.js');
require('./watch.js');
require('./webpack.js');

View File

@@ -1,6 +1,6 @@
const gulpUtil = require('gulp-util');
const colors = require('ansi-colors');
module.exports = function errorHandler(error) {
gulpUtil.log(gulpUtil.colors.red(`Error (${error.plugin}): ${error.message}`));
console.log(colors.red(`Error (${error.plugin}): ${error.message}`));
this.emit('end');
};

View File

@@ -1,15 +0,0 @@
const path = require('path');
const rootPath = path.resolve(__dirname + '/../../src/');
module.exports = function(source) {
if (this.cacheable) {
this.cacheable();
}
const resourcePath = this.resourcePath.replace(rootPath, '');
const wrappedSource =`
<!-- begin ${resourcePath} -->
${source}
<!-- end ${resourcePath} -->`;
return wrappedSource;
};

View File

@@ -1,15 +1,15 @@
const root = './frontend/src/';
const root = './frontend/src';
const paths = {
src: {
root,
html: root + '*.html',
scripts: root + '**/*.js',
content: root + 'Content/',
fonts: root + 'Content/Fonts/',
images: root + 'Content/Images/',
html: `${root}/*.html`,
scripts: `${root}/**/*.js`,
content: `${root}/Content/`,
fonts: `${root}/Content/Fonts/`,
images: `${root}/Content/Images/`,
exclude: {
libs: `!${root}JsLibraries/**`
libs: `!${root}/JsLibraries/**`
}
},
dest: {

View File

@@ -1,15 +0,0 @@
var gulp = require('gulp');
var print = require('gulp-print').default;
var paths = require('./helpers/paths.js');
gulp.task('imageMin', () => {
var imagemin = require('gulp-imagemin');
return gulp.src(paths.src.images)
.pipe(imagemin({
progressive: false,
optimizationLevel: 4,
svgoPlugins: [{ removeViewBox: false }]
}))
.pipe(print())
.pipe(gulp.dest(paths.src.content + 'Images/'));
});

View File

@@ -1,104 +0,0 @@
// will download and run sonarr (server) in a non-windows enviroment
// you can use this if you don't care about the server code and just want to work
// with the web code.
var http = require('http');
var gulp = require('gulp');
var fs = require('fs');
var targz = require('tar.gz');
var del = require('del');
var spawn = require('child_process').spawn;
function download(url, dest, cb) {
console.log('Downloading ' + url + ' to ' + dest);
var file = fs.createWriteStream(dest);
http.get(url, function(response) {
response.pipe(file);
file.on('finish', function() {
console.log('Download completed');
file.close(cb);
});
});
}
function getLatest(cb) {
var branch = 'develop';
process.argv.forEach(function(val) {
var branchMatch = /branch=([\S]*)/.exec(val);
if (branchMatch && branchMatch.length > 1) {
branch = branchMatch[1];
}
});
var url = 'http://services.sonarr.tv/v1/update/' + branch + '?os=osx';
console.log('Checking for latest version:', url);
http.get(url, function(res) {
var data = '';
res.on('data', function(chunk) {
data += chunk;
});
res.on('end', function() {
var updatePackage = JSON.parse(data).updatePackage;
console.log('Latest version available: ' + updatePackage.version + ' Release Date: ' + updatePackage.releaseDate);
cb(updatePackage);
});
}).on('error', function(e) {
console.log('problem with request: ' + e.message);
});
}
function extract(source, dest, cb) {
console.log('extracting download page to ' + dest);
new targz().extract(source, dest, function(err) {
if (err) {
console.log(err);
}
console.log('Update package extracted.');
cb();
});
}
gulp.task('getSonarr', function() {
try {
fs.mkdirSync('./_start/');
} catch (e) {
if (e.code !== 'EEXIST') {
throw e;
}
}
getLatest(function(updatePackage) {
var packagePath = './_start/' + updatePackage.filename;
var dirName = './_start/' + updatePackage.version;
download(updatePackage.url, packagePath, function() {
extract(packagePath, dirName, function() {
// clean old binaries
console.log('Cleaning old binaries');
del.sync(['./_output/*', '!./_output/UI/']);
console.log('copying binaries to target');
gulp.src(dirName + '/NzbDrone/*.*')
.pipe(gulp.dest('./_output/'));
});
});
});
});
gulp.task('startSonarr', function() {
var ls = spawn('mono', ['--debug', './_output/NzbDrone.exe']);
ls.stdout.on('data', function(data) {
process.stdout.write(data);
});
ls.stderr.on('data', function(data) {
process.stdout.write(data);
});
ls.on('close', function(code) {
console.log('child process exited with code ' + code);
});
});

View File

@@ -1,13 +0,0 @@
const gulp = require('gulp');
const paths = require('./helpers/paths.js');
const stripbom = require('gulp-stripbom');
function stripBom(dest) {
gulp.src([paths.src.scripts, paths.src.exclude.libs])
.pipe(stripbom({ showLog: false }))
.pipe(gulp.dest(dest));
}
gulp.task('stripBom', () => {
stripBom(paths.src.root);
});

View File

@@ -1,27 +1,18 @@
const gulp = require('gulp');
const livereload = require('gulp-livereload');
const watch = require('gulp-watch');
const gulpWatch = require('gulp-watch');
const paths = require('./helpers/paths.js');
require('./copy.js');
require('./webpack.js');
function watchTask(glob, task) {
const options = {
name: `watch: ${task}`,
verbose: true
};
return watch(glob, options, () => {
gulp.start(task);
});
}
gulp.task('watch', ['copyHtml', 'copyFonts', 'copyImages', 'copyJs'], () => {
function watch() {
livereload.listen({ start: true });
gulp.start('webpackWatch');
gulp.task('webpackWatch')();
gulpWatch(paths.src.html, gulp.series('copyHtml'));
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
}
watchTask(paths.src.html, 'copyHtml');
watchTask(`${paths.src.fonts}**/*.*`, 'copyFonts');
watchTask(`${paths.src.images}**/*.*`, 'copyImages');
});
gulp.task('watch', gulp.series('build', watch));

View File

@@ -4,57 +4,38 @@ const livereload = require('gulp-livereload');
const path = require('path');
const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const browsers = require('../browsers');
const uiFolder = 'UI';
const root = path.join(__dirname, '..', 'src');
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
console.log('ROOT:', root);
console.log('Source Folder:', srcFolder);
console.log('isProduction:', isProduction);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
'../src/Styles/Variables/dimensions',
'../src/Styles/Variables/fonts',
'../src/Styles/Variables/animations'
'../src/Styles/Variables/animations',
'../src/Styles/Variables/zIndexes'
].map(require.resolve);
const extractCSSPlugin = new ExtractTextPlugin({
filename: path.join('_output', uiFolder, 'Content', 'styles.css'),
allChunks: true,
disable: false,
ignoreOrder: true
});
const plugins = [
extractCSSPlugin,
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: path.join('_output', uiFolder, 'Content', 'styles.css')
})
];
if (isProduction) {
plugins.push(new UglifyJSPlugin({
sourceMap: true,
uglifyOptions: {
mangle: false,
output: {
comments: false,
beautify: true
}
}
}));
}
const config = {
mode: isProduction ? 'production' : 'development',
devtool: '#source-map',
stats: {
@@ -73,8 +54,8 @@ const config = {
resolve: {
modules: [
root,
path.join(root, 'Shims'),
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
@@ -87,6 +68,10 @@ const config = {
sourceMapFilename: '[file].map'
},
optimization: {
chunkIds: 'named'
},
plugins,
resolveLoader: {
@@ -101,53 +86,56 @@ const config = {
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
loader: 'babel-loader',
query: {
plugins: ['transform-class-properties'],
presets: ['es2015', 'decorators-legacy', 'react', 'stage-2'],
env: {
development: {
plugins: ['transform-react-jsx-source']
use: [
{
loader: 'babel-loader',
options: {
configFile: `${frontendFolder}/babel.config.js`,
envName: isProduction ? 'production' : 'development',
presets: [
[
'@babel/preset-env',
{
modules: false,
loose: true,
debug: false,
useBuiltIns: 'entry',
targets: browsers
}
]
]
}
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: extractCSSPlugin.extract({
fallback: 'style-loader',
use: [
{
loader: 'css-variables-loader',
options: {
cssVarsFiles
}
},
{
loader: 'css-loader',
options: {
modules: true,
importLoaders: 1,
localIdentName: '[name]-[local]-[hash:base64:5]',
sourceMap: true
}
},
{
loader: 'postcss-loader',
options: {
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
localIdentName: '[name]/[local]/[hash:base64:5]',
modules: true
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
}
]
})
}
]
},
// Global styles
@@ -195,17 +183,16 @@ const config = {
};
gulp.task('webpack', () => {
return gulp.src('index.js')
.pipe(webpackStream(config))
.pipe(gulp.dest(''));
return webpackStream(config)
.pipe(gulp.dest('./'));
});
gulp.task('webpackWatch', () => {
config.watch = true;
return gulp.src('')
.pipe(webpackStream(config))
return webpackStream(config)
.on('error', errorHandler)
.pipe(gulp.dest(''))
.pipe(gulp.dest('./'))
.on('error', errorHandler)
.pipe(livereload())
.on('error', errorHandler);

View File

@@ -1,4 +1,5 @@
const reload = require('require-nocache')(module);
const browsers = require('./browsers');
module.exports = (ctx, configPath, options) => {
const config = {
@@ -14,17 +15,10 @@ module.exports = (ctx, configPath, options) => {
return Object.assign(acc, reload(vars));
}, {})
},
'postcss-color-function': {},
'postcss-nested': {},
autoprefixer: {
browsers: [
'Chrome >= 30',
'Firefox >= 30',
'Safari >= 6',
'Edge >= 12',
'Explorer >= 11',
'iOS >= 7',
'Android >= 4.4'
]
browsers
}
}
};

View File

@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import { align, icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
@@ -41,6 +42,18 @@ class Blacklist extends Component {
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>

View File

@@ -105,6 +105,14 @@ class BlacklistConnector extends Component {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
//
// Render
@@ -126,6 +134,7 @@ class BlacklistConnector extends Component {
}
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,

View File

@@ -1,18 +1,18 @@
.language,
.quality {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px;
}
.indexer {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}

View File

@@ -1,5 +1,5 @@
.description {
composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css';
composes: description from '~Components/DescriptionList/DescriptionListItemDescription.css';
overflow-wrap: break-word;
}

View File

@@ -231,6 +231,16 @@ function HistoryDetails(props) {
</DescriptionList>
);
}
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title="Name"
data={sourceTitle}
/>
</DescriptionList>
);
}
HistoryDetails.propTypes = {

View File

@@ -1,5 +1,5 @@
.markAsFailedButton {
composes: button from 'Components/Link/Button.css';
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

View File

@@ -5,6 +5,7 @@ import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
@@ -75,6 +76,16 @@ class History extends Component {
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}

View File

@@ -8,6 +8,7 @@ import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import withCurrentPage from 'Components/withCurrentPage';
import * as historyActions from 'Store/Actions/historyActions';
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
import History from './History';
function createMapStateToProps() {
@@ -28,7 +29,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
...historyActions,
fetchEpisodes,
clearEpisodes
clearEpisodes,
clearEpisodeFiles
};
class HistoryConnector extends Component {
@@ -68,6 +70,7 @@ class HistoryConnector extends Component {
unregisterPagePopulator(this.repopulate);
this.props.clearHistory();
this.props.clearEpisodes();
this.props.clearEpisodeFiles();
}
//
@@ -137,6 +140,7 @@ class HistoryConnector extends Component {
}
HistoryConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchHistory: PropTypes.func.isRequired,
gotoHistoryFirstPage: PropTypes.func.isRequired,
@@ -149,7 +153,8 @@ HistoryConnector.propTypes = {
setHistoryTableOption: PropTypes.func.isRequired,
clearHistory: PropTypes.func.isRequired,
fetchEpisodes: PropTypes.func.isRequired,
clearEpisodes: PropTypes.func.isRequired
clearEpisodes: PropTypes.func.isRequired,
clearEpisodeFiles: PropTypes.func.isRequired
};
export default withCurrentPage(

View File

@@ -1,5 +1,5 @@
.cell {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 35px;
text-align: center;

View File

@@ -1,23 +1,23 @@
.downloadClient {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px;
}
.indexer {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}
.releaseGroup {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 110px;
}
.details {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 30px;
}

View File

@@ -1,12 +1,12 @@
.torrent {
composes: label from 'Components/Label.css';
composes: label from '~Components/Label.css';
border-color: $torrentColor;
background-color: $torrentColor;
}
.usenet {
composes: label from 'Components/Label.css';
composes: label from '~Components/Label.css';
border-color: $usenetColor;
background-color: $usenetColor;

View File

@@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { icons } from 'Helpers/Props';
import { align, icons } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
@@ -16,6 +17,7 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
@@ -43,22 +45,27 @@ class Queue extends Component {
// before episodes start fetching or when episodes start fetching.
if (
(
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items)
) ||
(!this.props.isEpisodesFetching && nextProps.isEpisodesFetching)
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items) &&
nextProps.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({ selectedState: {} });
this.setState((state) => {
return removeOldSelectedState(state, prevProps.items);
});
return;
}
@@ -139,7 +146,7 @@ class Queue extends Component {
} = this.state;
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length);
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
const hasError = error || episodesError;
const selectedCount = this.getSelectedIds().length;
const disableSelectedActions = selectedCount === 0;
@@ -173,6 +180,21 @@ class Queue extends Component {
onPress={this.onRemoveSelectedPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
>
<TableOptionsModalWrapper
columns={columns}
{...otherProps}
optionsComponent={QueueOptionsConnector}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBodyConnector>

View File

@@ -164,6 +164,8 @@ class QueueConnector extends Component {
}
QueueConnector.propTypes = {
includeUnknownSeriesItems: PropTypes.bool.isRequired,
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired,

View File

@@ -1,23 +1,23 @@
.quality {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.protocol {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px;
}
.progress {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}

View File

@@ -10,6 +10,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SeasonEpisodeNumber from 'Episode/SeasonEpisodeNumber';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
@@ -71,9 +72,11 @@ class QueueRow extends Component {
errorMessage,
series,
episode,
language,
quality,
protocol,
indexer,
outputPath,
downloadClient,
estimatedCompletionTime,
timeleft,
@@ -204,6 +207,16 @@ class QueueRow extends Component {
);
}
if (name === 'language') {
return (
<TableRowCell key={name}>
<EpisodeLanguage
language={language}
/>
</TableRowCell>
);
}
if (name === 'quality') {
return (
<TableRowCell key={name}>
@@ -240,6 +253,22 @@ class QueueRow extends Component {
);
}
if (name === 'title') {
return (
<TableRowCell key={name}>
{title}
</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
{outputPath}
</TableRowCell>
);
}
if (name === 'estimatedCompletionTime') {
return (
<TimeleftCell
@@ -340,9 +369,11 @@ QueueRow.propTypes = {
errorMessage: PropTypes.string,
series: PropTypes.object,
episode: PropTypes.object,
language: PropTypes.object.isRequired,
quality: PropTypes.object.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
outputPath: PropTypes.string,
downloadClient: PropTypes.string,
estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string,

View File

@@ -1,5 +1,5 @@
.status {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 30px;
}

View File

@@ -116,6 +116,7 @@ function QueueStatusCell(props) {
title={title}
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
position={tooltipPositions.RIGHT}
canFlip={false}
/>
</TableRowCell>
);

View File

@@ -12,8 +12,12 @@ function createMapStateToProps() {
(state) => state.queue.options.includeUnknownSeriesItems,
(app, status, includeUnknownSeriesItems) => {
const {
errors,
warnings,
unknownErrors,
unknownWarnings,
count,
unknownCount
totalCount
} = status.item;
return {
@@ -21,7 +25,9 @@ function createMapStateToProps() {
isReconnecting: app.isReconnecting,
isPopulated: status.isPopulated,
...status.item,
count: includeUnknownSeriesItems ? count : count - unknownCount
count: includeUnknownSeriesItems ? totalCount : count,
errors: includeUnknownSeriesItems ? errors || unknownErrors : errors,
warnings: includeUnknownSeriesItems ? warnings || unknownWarnings : warnings
};
}
);

View File

@@ -1,5 +1,5 @@
.timeleft {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px;
}

View File

@@ -17,7 +17,7 @@
}
.searchInput {
composes: input from 'Components/Form/TextInput.css';
composes: input from '~Components/Form/TextInput.css';
height: 46px;
border-radius: 0;

View File

@@ -100,6 +100,7 @@ class AddNewSeries extends Component {
name="seriesLookup"
value={term}
placeholder="eg. Breaking Bad, tvdb:####"
autoFocus={true}
onChange={this.onSearchInputChange}
/>

View File

@@ -10,7 +10,7 @@ import AddNewSeries from './AddNewSeries';
function createMapStateToProps() {
return createSelector(
(state) => state.addSeries,
(state) => state.routing.location,
(state) => state.router.location,
(addSeries, location) => {
const { params } = parseUrl(location.search);

View File

@@ -36,28 +36,28 @@
}
.searchForMissingEpisodesContainer {
composes: container from 'Components/Form/CheckInput.css';
composes: container from '~Components/Form/CheckInput.css';
flex: 0 1 0;
}
.searchForMissingEpisodesInput {
composes: input from 'Components/Form/CheckInput.css';
composes: input from '~Components/Form/CheckInput.css';
margin-top: 0;
}
.modalFooter {
composes: modalFooter from 'Components/Modal/ModalFooter.css';
composes: modalFooter from '~Components/Modal/ModalFooter.css';
}
.addButton {
@add-mixin truncate;
composes: button from 'Components/Link/SpinnerButton.css';
composes: button from '~Components/Link/SpinnerButton.css';
}
.hideLanguageProfile {
composes: group from 'Components/Form/FormGroup.css';
composes: group from '~Components/Form/FormGroup.css';
display: none;
}

View File

@@ -70,7 +70,8 @@ class AddNewSeriesModalContent extends Component {
showLanguageProfile,
isSmallScreen,
onModalClose,
onInputChange
onInputChange,
...otherProps
} = this.props;
return (
@@ -87,7 +88,8 @@ class AddNewSeriesModalContent extends Component {
<ModalBody>
<div className={styles.container}>
{
!isSmallScreen &&
isSmallScreen ?
null :
<div className={styles.poster}>
<SeriesPoster
className={styles.poster}
@@ -98,11 +100,15 @@ class AddNewSeriesModalContent extends Component {
}
<div className={styles.info}>
<div className={styles.overview}>
{overview}
</div>
{
overview ?
<div className={styles.overview}>
{overview}
</div> :
null
}
<Form>
<Form {...otherProps}>
<FormGroup>
<FormLabel>Root Folder</FormLabel>

View File

@@ -78,12 +78,14 @@ class AddNewSeriesSearchResult extends Component {
{...linkProps}
>
{
!isSmallScreen &&
<SeriesPoster
className={styles.poster}
images={images}
size={250}
/>
isSmallScreen ?
null :
<SeriesPoster
className={styles.poster}
images={images}
size={250}
overflow={true}
/>
}
<div>
@@ -91,18 +93,22 @@ class AddNewSeriesSearchResult extends Component {
{title}
{
!title.contains(year) && !!year &&
<span className={styles.year}>({year})</span>
!title.contains(year) && year ?
<span className={styles.year}>
({year})
</span> :
null
}
{
isExistingSeries &&
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/>
isExistingSeries ?
<Icon
className={styles.alreadyExistsIcon}
name={icons.CHECK_CIRCLE}
size={36}
title="Already in your library"
/> :
null
}
</div>
@@ -115,27 +121,30 @@ class AddNewSeriesSearchResult extends Component {
</Label>
{
!!network &&
<Label size={sizes.LARGE}>
{network}
</Label>
network ?
<Label size={sizes.LARGE}>
{network}
</Label> :
null
}
{
!!seasonCount &&
<Label size={sizes.LARGE}>
{seasons}
</Label>
seasonCount ?
<Label size={sizes.LARGE}>
{seasons}
</Label> :
null
}
{
status === 'ended' &&
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
status === 'ended' ?
<Label
kind={kinds.DANGER}
size={sizes.LARGE}
>
Ended
</Label>
</Label> :
null
}
</div>

View File

@@ -14,7 +14,7 @@
}
.importButton {
composes: button from 'Components/Link/SpinnerButton.css';
composes: button from '~Components/Link/SpinnerButton.css';
height: 35px;
}
@@ -26,7 +26,7 @@
}
.loading {
composes: loading from 'Components/Loading/LoadingIndicator.css';
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin: 0 10px 0 12px;
text-align: left;

View File

@@ -1,11 +1,11 @@
.folder {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 1 0 200px;
}
.monitor {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 200px;
min-width: 185px;
@@ -13,28 +13,28 @@
.qualityProfile,
.languageProfile {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 250px;
min-width: 170px;
}
.seriesType {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 200px;
min-width: 120px;
}
.seasonFolder {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 150px;
min-width: 120px;
}
.series {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
flex: 0 1 400px;
min-width: 300px;

View File

@@ -1,16 +1,16 @@
.selectInput {
composes: input from 'Components/Form/CheckInput.css';
composes: input from '~Components/Form/CheckInput.css';
}
.folder {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 1 0 200px;
line-height: 36px;
}
.monitor {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 200px;
min-width: 185px;
@@ -18,35 +18,35 @@
.qualityProfile,
.languageProfile {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 250px;
min-width: 170px;
}
.seriesType {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 200px;
min-width: 120px;
}
.seasonFolder {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 150px;
min-width: 120px;
}
.series {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
flex: 0 1 400px;
min-width: 300px;
}
.hideLanguageProfile {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
display: none;
}

View File

@@ -1,3 +1,3 @@
.input {
composes: input from 'Components/Form/CheckInput.css';
composes: input from '~Components/Form/CheckInput.css';
}

View File

@@ -1,11 +1,6 @@
.tether {
z-index: 2000;
}
.button {
composes: link from 'Components/Link/Link.css';
composes: link from '~Components/Link/Link.css';
position: relative;
display: flex;
align-items: center;
padding: 6px 16px;
@@ -36,9 +31,10 @@
}
.contentContainer {
z-index: $popperZIndex;
margin-top: 4px;
padding: 0 8px;
width: 400px;
/* 400px container witdh with 8px padding on each side */
width: 384px;
}
.content {
@@ -65,7 +61,7 @@
}
.searchInput {
composes: input from 'Components/Form/TextInput.css';
composes: input from '~Components/Form/TextInput.css';
border-radius: 0;
}

View File

@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import TetherComponent from 'react-tether';
import { Manager, Popper, Reference } from 'react-popper';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { icons, kinds } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Portal from 'Components/Portal';
import FormInputButton from 'Components/Form/FormInputButton';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -12,19 +13,6 @@ import ImportSeriesSearchResultConnector from './ImportSeriesSearchResultConnect
import ImportSeriesTitle from './ImportSeriesTitle';
import styles from './ImportSeriesSelectSeries.css';
const tetherOptions = {
skipMoveElement: true,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
}
],
attachment: 'top center',
targetAttachment: 'bottom center'
};
class ImportSeriesSelectSeries extends Component {
//
@@ -34,6 +22,9 @@ class ImportSeriesSelectSeries extends Component {
super(props, context);
this._seriesLookupTimeout = null;
this._scheduleUpdate = null;
this._buttonId = getUniqueElememtId();
this._contentId = getUniqueElememtId();
this.state = {
term: props.id,
@@ -41,17 +32,15 @@ class ImportSeriesSelectSeries extends Component {
};
}
componentDidUpdate() {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
}
//
// Control
_setButtonRef = (ref) => {
this._buttonRef = ref;
}
_setContentRef = (ref) => {
this._contentRef = ref;
}
_addListener() {
window.addEventListener('click', this.onWindowClick);
}
@@ -64,14 +53,18 @@ class ImportSeriesSelectSeries extends Component {
// Listeners
onWindowClick = (event) => {
const button = ReactDOM.findDOMNode(this._buttonRef);
const content = ReactDOM.findDOMNode(this._contentRef);
const button = document.getElementById(this._buttonId);
const content = document.getElementById(this._contentId);
if (!button) {
if (!button || !content) {
return;
}
if (!button.contains(event.target) && content && !content.contains(event.target) && this.state.isOpen) {
if (
!button.contains(event.target) &&
!content.contains(event.target) &&
this.state.isOpen
) {
this.setState({ isOpen: false });
this._removeListener();
}
@@ -129,129 +122,158 @@ class ImportSeriesSelectSeries extends Component {
error.responseJSON.message;
return (
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions}
>
<Link
ref={this._setButtonRef}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpSeries && isQueued && !isPopulated &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
<Manager>
<Reference>
{({ ref }) => (
<div
ref={ref}
id={this._buttonId}
>
<Link
ref={ref}
className={styles.button}
component="div"
onPress={this.onPress}
>
{
isLookingUpSeries && isQueued && !isPopulated ?
<LoadingIndicator
className={styles.loading}
size={20}
/> :
null
}
{
isPopulated && selectedSeries && isExistingSeries &&
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
}
{
isPopulated && selectedSeries && isExistingSeries ?
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/> :
null
}
{
isPopulated && selectedSeries &&
<ImportSeriesTitle
title={selectedSeries.title}
year={selectedSeries.year}
network={selectedSeries.network}
isExistingSeries={isExistingSeries}
/>
}
{
isPopulated && selectedSeries ?
<ImportSeriesTitle
title={selectedSeries.title}
year={selectedSeries.year}
network={selectedSeries.network}
isExistingSeries={isExistingSeries}
/> :
null
}
{
isPopulated && !selectedSeries &&
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
{
isPopulated && !selectedSeries ?
<div className={styles.noMatches}>
<Icon
className={styles.warningIcon}
name={icons.WARNING}
kind={kinds.WARNING}
/>
No match found!
</div>
}
</div> :
null
}
{
!isFetching && !!error &&
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
{
!isFetching && !!error ?
<div>
<Icon
className={styles.warningIcon}
title={errorMessage}
name={icons.WARNING}
kind={kinds.WARNING}
/>
Search failed, please try again later.
</div>
}
</div> :
null
}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
{
this.state.isOpen &&
<div
ref={this._setContentRef}
className={styles.contentContainer}
>
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
<div className={styles.dropdownArrowContainer}>
<Icon
name={icons.CARET_DOWN}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
</Link>
</div>
)}
</Reference>
<div className={styles.results}>
<Portal>
<Popper
placement="bottom"
modifiers={{
preventOverflow: {
boundariesElement: 'viewport'
}
}}
>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
ref={ref}
id={this._contentId}
className={styles.contentContainer}
style={style}
>
{
items.map((item) => {
return (
<ImportSeriesSearchResultConnector
key={item.tvdbId}
tvdbId={item.tvdbId}
title={item.title}
year={item.year}
network={item.network}
onPress={this.onSeriesSelect}
/>
);
})
this.state.isOpen ?
<div className={styles.content}>
<div className={styles.searchContainer}>
<div className={styles.searchIconContainer}>
<Icon name={icons.SEARCH} />
</div>
<TextInput
className={styles.searchInput}
name={`${name}_textInput`}
value={this.state.term}
onChange={this.onSearchInputChange}
/>
<FormInputButton
kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH}
canSpin={true}
isSpinning={isFetching}
onPress={this.onRefreshPress}
>
<Icon name={icons.REFRESH} />
</FormInputButton>
</div>
<div className={styles.results}>
{
items.map((item) => {
return (
<ImportSeriesSearchResultConnector
key={item.tvdbId}
tvdbId={item.tvdbId}
title={item.title}
year={item.year}
network={item.network}
onPress={this.onSeriesSelect}
/>
);
})
}
</div>
</div> :
null
}
</div>
</div>
</div>
}
</TetherComponent>
);
}}
</Popper>
</Portal>
</Manager>
);
}
}

View File

@@ -1,18 +0,0 @@
.link {
composes: link from 'Components/Link/Link.css';
display: block;
}
.freeSpace,
.unmappedFolders {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.actions {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 45px;
}

View File

@@ -1,48 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
import ImportSeriesRootFolderRow from './ImportSeriesRootFolderRow';
function createMapStateToProps() {
return createSelector(
() => {
return {
};
}
);
}
const mapDispatchToProps = {
deleteRootFolder
};
class ImportSeriesRootFolderRowConnector extends Component {
//
// Listeners
onDeletePress = () => {
this.props.deleteRootFolder({ id: this.props.id });
}
//
// Render
render() {
return (
<ImportSeriesRootFolderRow
{...this.props}
onDeletePress={this.onDeletePress}
/>
);
}
}
ImportSeriesRootFolderRowConnector.propTypes = {
id: PropTypes.number.isRequired,
deleteRootFolder: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(ImportSeriesRootFolderRowConnector);

View File

@@ -2,39 +2,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import FieldSet from 'Components/FieldSet';
import Icon from 'Components/Icon';
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 Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import ImportSeriesRootFolderRowConnector from './ImportSeriesRootFolderRowConnector';
import RootFolders from 'RootFolder/RootFolders';
import styles from './ImportSeriesSelectFolder.css';
const rootFolderColumns = [
{
name: 'path',
label: 'Path',
isVisible: true
},
{
name: 'freeSpace',
label: 'Free Space',
isVisible: true
},
{
name: 'unmappedFolders',
label: 'Unmapped Folders',
isVisible: true
},
{
name: 'actions',
isVisible: true
}
];
class ImportSeriesSelectFolder extends Component {
//
@@ -110,26 +86,13 @@ class ImportSeriesSelectFolder extends Component {
{
items.length > 0 ?
<div className={styles.recentFolders}>
<FieldSet legend="Recent Folders">
<Table
columns={rootFolderColumns}
>
<TableBody>
{
items.map((rootFolder) => {
return (
<ImportSeriesRootFolderRowConnector
key={rootFolder.id}
id={rootFolder.id}
path={rootFolder.path}
freeSpace={rootFolder.freeSpace}
unmappedFolders={rootFolder.unmappedFolders}
/>
);
})
}
</TableBody>
</Table>
<FieldSet legend="Root Folders">
<RootFolders
isFetching={isFetching}
isPopulated={isPopulated}
error={error}
items={items}
/>
</FieldSet>
<Button
@@ -181,8 +144,7 @@ ImportSeriesSelectFolder.propTypes = {
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired,
onDeleteRootFolderPress: PropTypes.func.isRequired
onNewRootFolderSelect: PropTypes.func.isRequired
};
export default ImportSeriesSelectFolder;

View File

@@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { push } from 'react-router-redux';
import { push } from 'connected-react-router';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions';
import { fetchRootFolders, addRootFolder } from 'Store/Actions/rootFolderActions';
import ImportSeriesSelectFolder from './ImportSeriesSelectFolder';
function createMapStateToProps() {
@@ -24,7 +24,6 @@ function createMapStateToProps() {
const mapDispatchToProps = {
fetchRootFolders,
addRootFolder,
deleteRootFolder,
push
};
@@ -60,10 +59,6 @@ class ImportSeriesSelectFolderConnector extends Component {
this.props.addRootFolder({ path });
}
onDeleteRootFolderPress = (id) => {
this.props.deleteRootFolder({ id });
}
//
// Render
@@ -72,7 +67,6 @@ class ImportSeriesSelectFolderConnector extends Component {
<ImportSeriesSelectFolder
{...this.props}
onNewRootFolderSelect={this.onNewRootFolderSelect}
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
/>
);
}
@@ -84,7 +78,6 @@ ImportSeriesSelectFolderConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchRootFolders: PropTypes.func.isRequired,
addRootFolder: PropTypes.func.isRequired,
deleteRootFolder: PropTypes.func.isRequired,
push: PropTypes.func.isRequired
};

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import { ConnectedRouter } from 'connected-react-router';
import PageConnector from 'Components/Page/PageConnector';
import AppRoutes from './AppRoutes';

View File

@@ -0,0 +1,6 @@
import React from 'react';
const ColorImpairedContext = React.createContext(false);
export const ColorImpairedConsumer = ColorImpairedContext.Consumer;
export default ColorImpairedContext;

View File

@@ -63,27 +63,27 @@
*/
.downloaded {
composes: downloaded from 'Calendar/Events/CalendarEvent.css';
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
}
.downloading {
composes: downloading from 'Calendar/Events/CalendarEvent.css';
composes: downloading from '~Calendar/Events/CalendarEvent.css';
}
.unmonitored {
composes: unmonitored from 'Calendar/Events/CalendarEvent.css';
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
}
.onAir {
composes: onAir from 'Calendar/Events/CalendarEvent.css';
composes: onAir from '~Calendar/Events/CalendarEvent.css';
}
.missing {
composes: missing from 'Calendar/Events/CalendarEvent.css';
composes: missing from '~Calendar/Events/CalendarEvent.css';
}
.premiere {
composes: premiere from 'Calendar/Events/CalendarEvent.css';
composes: premiere from '~Calendar/Events/CalendarEvent.css';
}
@media only screen and (max-width: $breakpointSmall) {

View File

@@ -175,6 +175,7 @@ class CalendarConnector extends Component {
}
CalendarConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
time: PropTypes.string,
view: PropTypes.string.isRequired,
firstDayOfWeek: PropTypes.number.isRequired,

View File

@@ -1,11 +1,11 @@
.calendarPageBody {
composes: contentBody from 'Components/Page/PageContentBody.css';
composes: contentBody from '~Components/Page/PageContentBody.css';
display: flex;
}
.calendarInnerPageBody {
composes: innerContentBody from 'Components/Page/PageContentBody.css';
composes: innerContentBody from '~Components/Page/PageContentBody.css';
display: flex;
flex-direction: column;

View File

@@ -25,7 +25,7 @@ function createMissingEpisodeIdsSelector() {
moment(airDateUtc).isAfter(start) &&
moment(airDateUtc).isBefore(end) &&
isBefore(episode.airDateUtc) &&
!queueDetails.some((details) => details.episode.id === episode.id)
!queueDetails.some((details) => !!details.episode && details.episode.id === episode.id)
) {
acc.push(episode.id);
}

View File

@@ -5,6 +5,10 @@
border-bottom: 1px solid $borderColor;
border-left: 4px solid $borderColor;
font-size: 12px;
&:global(.colorImpaired) {
border-left-width: 5px;
}
}
.info,
@@ -39,6 +43,10 @@
.downloaded {
border-left-color: $successColor !important;
&:global(.colorImpaired) {
border-left-color: color($successColor, saturation(+15%)) !important;
}
}
.downloading {
@@ -49,7 +57,7 @@
border-left-color: $gray !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -57,7 +65,7 @@
border-left-color: $warningColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -65,7 +73,8 @@
border-left-color: $dangerColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -73,6 +82,6 @@
border-left-color: $primaryColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}

View File

@@ -58,25 +58,25 @@
*/
.downloaded {
composes: downloaded from 'Calendar/Events/CalendarEvent.css';
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
}
.downloading {
composes: downloading from 'Calendar/Events/CalendarEvent.css';
composes: downloading from '~Calendar/Events/CalendarEvent.css';
}
.unmonitored {
composes: unmonitored from 'Calendar/Events/CalendarEvent.css';
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
}
.onAir {
composes: onAir from 'Calendar/Events/CalendarEvent.css';
composes: onAir from '~Calendar/Events/CalendarEvent.css';
}
.missing {
composes: missing from 'Calendar/Events/CalendarEvent.css';
composes: missing from '~Calendar/Events/CalendarEvent.css';
}
.premiere {
composes: premiere from 'Calendar/Events/CalendarEvent.css';
composes: premiere from '~Calendar/Events/CalendarEvent.css';
}

View File

@@ -10,7 +10,7 @@ function createIsDownloadingSelector() {
(state) => state.queue.details,
(episodeIds, details) => {
return details.items.some((item) => {
return episodeIds.includes(item.episode.id);
return item.episode && episodeIds.includes(item.episode.id);
});
}
);

View File

@@ -8,7 +8,7 @@
}
.todayButton {
composes: button from 'Components/Link/Button.css';
composes: button from '~Components/Link/Button.css';
margin-left: 5px;
}
@@ -30,13 +30,13 @@
}
.viewMenu {
composes: menu from 'Components/Menu/Menu.css';
composes: menu from '~Components/Menu/Menu.css';
line-height: 31px;
}
.loading {
composes: loading from 'Components/Loading/LoadingIndicator.css';
composes: loading from '~Components/Loading/LoadingIndicator.css';
margin-top: 5px;
margin-right: 10px;

View File

@@ -83,6 +83,7 @@ class CalendarHeader extends Component {
end,
longDateFormat,
isSmallScreen,
collapseViewButtons,
onTodayPress,
onPreviousPress,
onNextPress
@@ -145,7 +146,7 @@ class CalendarHeader extends Component {
}
{
isSmallScreen ?
collapseViewButtons ?
<Menu
className={styles.viewMenu}
alignMenu={align.RIGHT}
@@ -158,6 +159,18 @@ class CalendarHeader extends Component {
</MenuButton>
<MenuContent>
{
isSmallScreen ?
null :
<ViewMenuItem
name={calendarViews.MONTH}
selectedView={view}
onPress={this.onViewChange}
>
Month
</ViewMenuItem>
}
<ViewMenuItem
name={calendarViews.WEEK}
selectedView={view}
@@ -243,6 +256,7 @@ CalendarHeader.propTypes = {
end: PropTypes.string.isRequired,
view: PropTypes.oneOf(calendarViews.all).isRequired,
isSmallScreen: PropTypes.bool.isRequired,
collapseViewButtons: PropTypes.bool.isRequired,
longDateFormat: PropTypes.string.isRequired,
onViewChange: PropTypes.func.isRequired,
onTodayPress: PropTypes.func.isRequired,

View File

@@ -23,6 +23,7 @@ function createMapStateToProps() {
]);
result.isSmallScreen = dimensions.isSmallScreen;
result.collapseViewButtons = dimensions.isLargeScreen;
result.longDateFormat = uiSettings.longDateFormat;
return result;

View File

@@ -63,6 +63,21 @@ function Legend(props) {
/>
</div>
<div>
<LegendItem
status="onAir"
name="On Air"
tooltip="Episode is currently airing"
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="missing"
tooltip="Episode has aired and is missing from disk"
colorImpairedMode={colorImpairedMode}
/>
</div>
<div>
<LegendItem
status="downloading"

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import styles from './legendIconItem.css';
import styles from './LegendIconItem.css';
function LegendIconItem(props) {
const {

View File

@@ -13,29 +13,29 @@
*/
.downloaded {
composes: downloaded from 'Calendar/Events/CalendarEvent.css';
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
}
.downloading {
composes: downloading from 'Calendar/Events/CalendarEvent.css';
composes: downloading from '~Calendar/Events/CalendarEvent.css';
}
.unmonitored {
composes: unmonitored from 'Calendar/Events/CalendarEvent.css';
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
}
.onAir {
composes: onAir from 'Calendar/Events/CalendarEvent.css';
composes: onAir from '~Calendar/Events/CalendarEvent.css';
}
.missing {
composes: missing from 'Calendar/Events/CalendarEvent.css';
composes: missing from '~Calendar/Events/CalendarEvent.css';
}
.premiere {
composes: premiere from 'Calendar/Events/CalendarEvent.css';
composes: premiere from '~Calendar/Events/CalendarEvent.css';
}
.unaired {
composes: unaired from 'Calendar/Events/CalendarEvent.css';
composes: unaired from '~Calendar/Events/CalendarEvent.css';
}

View File

@@ -1,5 +1,5 @@
.modal {
composes: modal from 'Components/Modal/Modal.css';
composes: modal from '~Components/Modal/Modal.css';
height: 600px;
}

View File

@@ -1,12 +1,12 @@
.modalBody {
composes: modalBody from 'Components/Modal/ModalBody.css';
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
flex-direction: column;
}
.mappedDrivesWarning {
composes: alert from 'Components/Alert.css';
composes: alert from '~Components/Alert.css';
margin: 0;
margin-bottom: 20px;
@@ -18,7 +18,7 @@
}
.pathInput {
composes: pathInputWrapper from 'Components/Form/PathInput.css';
composes: inputWrapper from '~Components/Form/PathInput.css';
flex: 0 0 auto;
}

View File

@@ -42,8 +42,8 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
fetchPaths,
clearPaths
dispatchFetchPaths: fetchPaths,
dispatchClearPaths: clearPaths
};
class FileBrowserModalContentConnector extends Component {
@@ -51,9 +51,16 @@ class FileBrowserModalContentConnector extends Component {
// Lifecycle
componentDidMount() {
this.props.fetchPaths({
path: this.props.value,
allowFoldersWithoutTrailingSlashes: true
const {
value,
includeFiles,
dispatchFetchPaths
} = this.props;
dispatchFetchPaths({
path: value,
allowFoldersWithoutTrailingSlashes: true,
includeFiles
});
}
@@ -61,18 +68,24 @@ class FileBrowserModalContentConnector extends Component {
// Listeners
onFetchPaths = (path) => {
this.props.fetchPaths({
const {
includeFiles,
dispatchFetchPaths
} = this.props;
dispatchFetchPaths({
path,
allowFoldersWithoutTrailingSlashes: true
allowFoldersWithoutTrailingSlashes: true,
includeFiles
});
}
onClearPaths = () => {
// this.props.clearPaths();
// this.props.dispatchClearPaths();
}
onModalClose = () => {
this.props.clearPaths();
this.props.dispatchClearPaths();
this.props.onModalClose();
}
@@ -93,9 +106,14 @@ class FileBrowserModalContentConnector extends Component {
FileBrowserModalContentConnector.propTypes = {
value: PropTypes.string,
fetchPaths: PropTypes.func.isRequired,
clearPaths: PropTypes.func.isRequired,
includeFiles: PropTypes.bool.isRequired,
dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
FileBrowserModalContentConnector.defaultProps = {
includeFiles: false
};
export default connect(createMapStateToProps, mapDispatchToProps)(FileBrowserModalContentConnector);

View File

@@ -1,5 +1,5 @@
.type {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 32px;
}

View File

@@ -3,13 +3,13 @@
}
.numberInput {
composes: input from 'Components/Form/TextInput.css';
composes: input from '~Components/Form/TextInput.css';
margin-right: 3px;
}
.selectInput {
composes: select from 'Components/Form/SelectInput.css';
composes: select from '~Components/Form/SelectInput.css';
margin-left: 3px;
}

View File

@@ -3,7 +3,8 @@ import React, { Component } from 'react';
import convertToBytes from 'Utilities/Number/convertToBytes';
import formatBytes from 'Utilities/Number/formatBytes';
import { kinds, filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
import TagInput, { tagShape } from 'Components/Form/TagInput';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import TagInput from 'Components/Form/TagInput';
import FilterBuilderRowValueTag from './FilterBuilderRowValueTag';
export const NAME = 'value';

View File

@@ -7,7 +7,7 @@
}
.label {
composes: label from 'Components/Label.css';
composes: label from '~Components/Label.css';
border-style: none;
font-size: 13px;

View File

@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import { fetchIndexers } from 'Store/Actions/settingsActions';
import { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {

View File

@@ -3,8 +3,8 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import getQualities from 'Utilities/Quality/getQualities';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
import { tagShape } from 'Components/Form/TagInput';
import FilterBuilderRowValue from './FilterBuilderRowValue';
function createMapStateToProps() {

View File

@@ -1,9 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import classNames from 'classnames';
import jdu from 'jdu';
import styles from './AutoCompleteInput.css';
import AutoSuggestInput from './AutoSuggestInput';
class AutoCompleteInput extends Component {
@@ -39,31 +37,6 @@ class AutoCompleteInput extends Component {
});
}
onInputKeyDown = (event) => {
const {
name,
value,
onChange
} = this.props;
const { suggestions } = this.state;
if (
event.key === 'Tab' &&
suggestions.length &&
suggestions[0] !== this.props.value
) {
event.preventDefault();
if (value) {
onChange({
name,
value: suggestions[0]
});
}
}
}
onInputBlur = () => {
this.setState({ suggestions: [] });
}
@@ -88,74 +61,37 @@ class AutoCompleteInput extends Component {
render() {
const {
className,
inputClassName,
name,
value,
placeholder,
hasError,
hasWarning
...otherProps
} = this.props;
const { suggestions } = this.state;
const inputProps = {
className: classNames(
inputClassName,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
),
name,
value,
placeholder,
autoComplete: 'off',
spellCheck: false,
onChange: this.onInputChange,
onKeyDown: this.onInputKeyDown,
onBlur: this.onInputBlur
};
const theme = {
container: styles.inputContainer,
containerOpen: styles.inputContainerOpen,
suggestionsContainer: styles.container,
suggestionsList: styles.list,
suggestion: styles.listItem,
suggestionHighlighted: styles.highlighted
};
return (
<div className={className}>
<Autosuggest
id={name}
inputProps={inputProps}
theme={theme}
suggestions={suggestions}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
/>
</div>
<AutoSuggestInput
{...otherProps}
name={name}
value={value}
suggestions={suggestions}
getSuggestionValue={this.getSuggestionValue}
renderSuggestion={this.renderSuggestion}
onInputBlur={this.onInputBlur}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
/>
);
}
}
AutoCompleteInput.propTypes = {
className: PropTypes.string.isRequired,
inputClassName: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string,
values: PropTypes.arrayOf(PropTypes.string).isRequired,
placeholder: PropTypes.string,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
onChange: PropTypes.func.isRequired
};
AutoCompleteInput.defaultProps = {
className: styles.inputWrapper,
inputClassName: styles.input,
value: ''
};

View File

@@ -1,34 +1,29 @@
.input {
composes: input from 'Components/Form/Input.css';
composes: input from '~Components/Form/Input.css';
}
.hasError {
composes: hasError from 'Components/Form/Input.css';
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from 'Components/Form/Input.css';
}
.inputWrapper {
display: flex;
composes: hasWarning from '~Components/Form/Input.css';
}
.inputContainer {
position: relative;
flex-grow: 1;
}
.container {
.suggestionsContainer {
@add-mixin scrollbar;
@add-mixin scrollbarTrack;
@add-mixin scrollbarThumb;
}
.inputContainerOpen {
.container {
position: absolute;
z-index: 1;
.suggestionsContainerOpen {
z-index: $popperZIndex;
.suggestionsContainer {
overflow-y: auto;
max-height: 200px;
width: 100%;
@@ -39,20 +34,16 @@
}
}
.list {
.suggestionsList {
margin: 5px 0;
padding-left: 0;
list-style-type: none;
}
.listItem {
.suggestion {
padding: 0 16px;
}
.match {
font-weight: bold;
}
.highlighted {
.suggestionHighlighted {
background-color: $menuItemHoverBackgroundColor;
}

View File

@@ -0,0 +1,257 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Autosuggest from 'react-autosuggest';
import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames';
import Portal from 'Components/Portal';
import styles from './AutoSuggestInput.css';
class AutoSuggestInput extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this._scheduleUpdate = null;
}
componentDidUpdate(prevProps) {
if (
this._scheduleUpdate &&
prevProps.suggestions !== this.props.suggestions
) {
this._scheduleUpdate();
}
}
//
// Control
renderInputComponent = (inputProps) => {
const { renderInputComponent } = this.props;
return (
<Reference>
{({ ref }) => {
if (renderInputComponent) {
return renderInputComponent(inputProps, ref);
}
return (
<div ref={ref}>
<input
{...inputProps}
/>
</div>
);
}}
</Reference>
);
}
renderSuggestionsContainer = ({ containerProps, children }) => {
return (
<Portal>
<Popper
placement='bottom-start'
modifiers={{
computeMaxHeight: {
order: 851,
enabled: true,
fn: this.onComputeMaxHeight
},
flip: {
padding: this.props.minHeight
}
}}
>
{({ ref: popperRef, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
return (
<div
ref={popperRef}
style={style}
className={children ? styles.suggestionsContainerOpen : undefined}
>
<div
{...containerProps}
style={{
maxHeight: style.maxHeight
}}
>
{children}
</div>
</div>
);
}}
</Popper>
</Portal>
);
}
//
// Listeners
onComputeMaxHeight = (data) => {
const {
top,
bottom,
width
} = data.offsets.reference;
const windowHeight = window.innerHeight;
if ((/^botton/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
data.styles.width = width;
return data;
}
onInputChange = (event, { newValue }) => {
this.props.onChange({
name: this.props.name,
value: newValue
});
}
onInputKeyDown = (event) => {
const {
name,
value,
suggestions,
onChange
} = this.props;
if (
event.key === 'Tab' &&
suggestions.length &&
suggestions[0] !== this.props.value
) {
event.preventDefault();
if (value) {
onChange({
name,
value: suggestions[0]
});
}
}
}
//
// Render
render() {
const {
forwardedRef,
className,
inputContainerClassName,
name,
value,
placeholder,
suggestions,
hasError,
hasWarning,
getSuggestionValue,
renderSuggestion,
onInputChange,
onInputKeyDown,
onInputFocus,
onInputBlur,
onSuggestionsFetchRequested,
onSuggestionsClearRequested,
onSuggestionSelected,
...otherProps
} = this.props;
const inputProps = {
className: classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
),
name,
value,
placeholder,
autoComplete: 'off',
spellCheck: false,
onChange: onInputChange || this.onInputChange,
onKeyDown: onInputKeyDown || this.onInputKeyDown,
onFocus: onInputFocus,
onBlur: onInputBlur
};
const theme = {
container: inputContainerClassName,
containerOpen: styles.suggestionsContainerOpen,
suggestionsContainer: styles.suggestionsContainer,
suggestionsList: styles.suggestionsList,
suggestion: styles.suggestion,
suggestionHighlighted: styles.suggestionHighlighted
};
return (
<Manager>
<Autosuggest
{...otherProps}
ref={forwardedRef}
id={name}
inputProps={inputProps}
theme={theme}
suggestions={suggestions}
getSuggestionValue={getSuggestionValue}
renderInputComponent={this.renderInputComponent}
renderSuggestionsContainer={this.renderSuggestionsContainer}
renderSuggestion={renderSuggestion}
onSuggestionSelected={onSuggestionSelected}
onSuggestionsFetchRequested={onSuggestionsFetchRequested}
onSuggestionsClearRequested={onSuggestionsClearRequested}
/>
</Manager>
);
}
}
AutoSuggestInput.propTypes = {
forwardedRef: PropTypes.func,
className: PropTypes.string.isRequired,
inputContainerClassName: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
placeholder: PropTypes.string,
suggestions: PropTypes.array.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
enforceMaxHeight: PropTypes.bool.isRequired,
minHeight: PropTypes.number.isRequired,
maxHeight: PropTypes.number.isRequired,
getSuggestionValue: PropTypes.func.isRequired,
renderInputComponent: PropTypes.func,
renderSuggestion: PropTypes.func.isRequired,
onInputChange: PropTypes.func,
onInputKeyDown: PropTypes.func,
onInputFocus: PropTypes.func,
onInputBlur: PropTypes.func.isRequired,
onSuggestionsFetchRequested: PropTypes.func.isRequired,
onSuggestionsClearRequested: PropTypes.func.isRequired,
onSuggestionSelected: PropTypes.func,
onChange: PropTypes.func.isRequired
};
AutoSuggestInput.defaultProps = {
className: styles.input,
inputContainerClassName: styles.inputContainer,
enforceMaxHeight: true,
minHeight: 50,
maxHeight: 200
};
export default AutoSuggestInput;

View File

@@ -3,19 +3,19 @@
}
.input {
composes: input from 'Components/Form/Input.css';
composes: input from '~Components/Form/Input.css';
}
.hasError {
composes: hasError from 'Components/Form/Input.css';
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from 'Components/Form/Input.css';
composes: hasWarning from '~Components/Form/Input.css';
}
.hasButton {
composes: hasButton from 'Components/Form/Input.css';
composes: hasButton from '~Components/Form/Input.css';
}
.recaptchaWrapper {

View File

@@ -94,7 +94,7 @@
}
.helpText {
composes: helpText from 'Components/Form/FormInputHelpText.css';
composes: helpText from '~Components/Form/FormInputHelpText.css';
margin-top: 8px;
margin-left: 5px;

View File

@@ -2,7 +2,7 @@
display: flex;
}
.inputContainer {
composes: inputContainer from './TagInput.css';
composes: hasButton from 'Components/Form/Input.css';
.input {
composes: input from '~./TagInput.css';
composes: hasButton from '~Components/Form/Input.css';
}

View File

@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import Icon from 'Components/Icon';
import FormInputButton from './FormInputButton';
import TagInput, { tagShape } from './TagInput';
import TagInput from './TagInput';
import styles from './DeviceInput.css';
class DeviceInput extends Component {
@@ -46,6 +47,7 @@ class DeviceInput extends Component {
render() {
const {
className,
name,
items,
selectedDevices,
hasError,
@@ -57,7 +59,8 @@ class DeviceInput extends Component {
return (
<div className={className}>
<TagInput
className={styles.inputContainer}
inputContainerClassName={styles.input}
name={name}
tags={selectedDevices}
tagList={items}
allowNew={true}

View File

@@ -1,10 +1,6 @@
.tether {
z-index: 2000;
}
.enhancedSelect {
composes: input from 'Components/Form/Input.css';
composes: link from 'Components/Link/Link.css';
composes: input from '~Components/Form/Input.css';
composes: link from '~Components/Link/Link.css';
position: relative;
display: flex;
@@ -21,11 +17,11 @@
}
.hasError {
composes: hasError from 'Components/Form/Input.css';
composes: hasError from '~Components/Form/Input.css';
}
.hasWarning {
composes: hasWarning from 'Components/Form/Input.css';
composes: hasWarning from '~Components/Form/Input.css';
}
.isDisabled {
@@ -44,10 +40,13 @@
}
.optionsContainer {
z-index: $popperZIndex;
width: auto;
}
.options {
composes: scroller from '~Components/Scroller/Scroller.css';
border: 1px solid $inputBorderColor;
border-radius: 4px;
background-color: $white;
@@ -62,7 +61,7 @@
}
.optionsModalBody {
composes: modalBody from 'Components/Modal/ModalBody.css';
composes: modalBody from '~Components/Modal/ModalBody.css';
display: flex;
justify-content: center;
@@ -71,7 +70,7 @@
}
.optionsModalScroller {
composes: scroller from 'Components/Scroller/Scroller.css';
composes: scroller from '~Components/Scroller/Scroller.css';
border: 1px solid $inputBorderColor;
border-radius: 4px;

View File

@@ -1,35 +1,23 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import TetherComponent from 'react-tether';
import { Manager, Popper, Reference } from 'react-popper';
import classNames from 'classnames';
import getUniqueElememtId from 'Utilities/getUniqueElementId';
import isMobileUtil from 'Utilities/isMobile';
import * as keyCodes from 'Utilities/Constants/keyCodes';
import { icons, scrollDirections } from 'Helpers/Props';
import { icons, sizes, scrollDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Portal from 'Components/Portal';
import Link from 'Components/Link/Link';
import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import Scroller from 'Components/Scroller/Scroller';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import HintedSelectInputOption from './HintedSelectInputOption';
import styles from './EnhancedSelectInput.css';
const tetherOptions = {
skipMoveElement: true,
constraints: [
{
to: 'window',
attachment: 'together',
pin: true
}
],
attachment: 'top left',
targetAttachment: 'bottom left'
};
function isArrowKey(keyCode) {
return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW;
}
@@ -87,6 +75,10 @@ class EnhancedSelectInput extends Component {
constructor(props, context) {
super(props, context);
this._scheduleUpdate = null;
this._buttonId = getUniqueElememtId();
this._optionsId = getUniqueElememtId();
this.state = {
isOpen: false,
selectedIndex: getSelectedIndex(props),
@@ -96,6 +88,10 @@ class EnhancedSelectInput extends Component {
}
componentDidUpdate(prevProps) {
if (this._scheduleUpdate) {
this._scheduleUpdate();
}
if (prevProps.value !== this.props.value) {
this.setState({
selectedIndex: getSelectedIndex(this.props)
@@ -106,14 +102,6 @@ class EnhancedSelectInput extends Component {
//
// Control
_setButtonRef = (ref) => {
this._buttonRef = ref;
}
_setOptionsRef = (ref) => {
this._optionsRef = ref;
}
_addListener() {
window.addEventListener('click', this.onWindowClick);
}
@@ -125,9 +113,26 @@ class EnhancedSelectInput extends Component {
//
// Listeners
onComputeMaxHeight = (data) => {
const {
top,
bottom
} = data.offsets.reference;
const windowHeight = window.innerHeight;
if ((/^botton/).test(data.placement)) {
data.styles.maxHeight = windowHeight - bottom;
} else {
data.styles.maxHeight = top;
}
return data;
}
onWindowClick = (event) => {
const button = ReactDOM.findDOMNode(this._buttonRef);
const options = ReactDOM.findDOMNode(this._optionsRef);
const button = document.getElementById(this._buttonId);
const options = document.getElementById(this._optionsId);
if (!button || this.state.isMobile) {
return;
@@ -145,9 +150,11 @@ class EnhancedSelectInput extends Component {
}
onBlur = () => {
this.setState({
selectedIndex: getSelectedIndex(this.props)
});
// Calling setState without this check prevents the click event from being properly handled on Chrome (it is on firefox)
const origIndex = getSelectedIndex(this.props);
if (origIndex !== this.state.selectedIndex) {
this.setState({ selectedIndex: origIndex });
}
}
onKeyDown = (event) => {
@@ -271,85 +278,116 @@ class EnhancedSelectInput extends Component {
return (
<div>
<TetherComponent
classes={{
element: styles.tether
}}
{...tetherOptions}
>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Link
ref={this._setButtonRef}
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<Manager>
<Reference>
{({ ref }) => (
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
ref={ref}
id={this._buttonId}
>
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</Measure>
</div>
)}
</Reference>
<Portal>
<Popper
placement="bottom-start"
modifiers={{
computeMaxHeight: {
order: 851,
enabled: true,
fn: this.onComputeMaxHeight
}
>
<Icon
name={icons.CARET_DOWN}
/>
</div>
</Link>
</Measure>
}}
>
{({ ref, style, scheduleUpdate }) => {
this._scheduleUpdate = scheduleUpdate;
{
isOpen && !isMobile &&
<div
ref={this._setOptionsRef}
className={styles.optionsContainer}
style={{
minWidth: width
}}
>
<div className={styles.options}>
{
values.map((v, index) => {
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...v}
isMobile={false}
onSelect={this.onSelect}
return (
<div
ref={ref}
id={this._optionsId}
className={styles.optionsContainer}
style={{
...style,
minWidth: width
}}
>
{
isOpen && !isMobile ?
<Scroller
className={styles.options}
style={{
maxHeight: style.maxHeight
}}
>
{v.value}
</OptionComponent>
);
})
}
</div>
</div>
}
</TetherComponent>
{
values.map((v, index) => {
return (
<OptionComponent
key={v.key}
id={v.key}
isSelected={index === selectedIndex}
{...v}
isMobile={false}
onSelect={this.onSelect}
>
{v.value}
</OptionComponent>
);
})
}
</Scroller> :
null
}
</div>
);
}
}
</Popper>
</Portal>
</Manager>
{
isMobile &&
<Modal
className={styles.optionsModal}
size={sizes.EXTRA_SMALL}
isOpen={isOpen}
onModalClose={this.onOptionsModalClose}
>
@@ -404,8 +442,8 @@ EnhancedSelectInput.defaultProps = {
disabledClassName: styles.isDisabled,
isDisabled: false,
selectedValueOptions: {},
selectedValueComponent: EnhancedSelectInputSelectedValue,
optionComponent: EnhancedSelectInputOption
selectedValueComponent: HintedSelectInputSelectedValue,
optionComponent: HintedSelectInputOption
};
export default EnhancedSelectInput;

View File

@@ -7,13 +7,17 @@
cursor: default;
&:hover {
background-color: #f9f9f9;
background-color: #f8f8f8;
}
}
.isSelected {
background-color: #e2e2e2;
&:hover {
background-color: #e2e2e2;
}
&.isMobile {
background-color: inherit;

View File

@@ -0,0 +1,3 @@
.validationFailures {
margin-bottom: 20px;
}

View File

@@ -2,37 +2,42 @@ import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
import Alert from 'Components/Alert';
import styles from './Form.css';
function Form({ children, validationErrors, validationWarnings, ...otherProps }) {
return (
<div>
<div>
{
validationErrors.map((error, index) => {
return (
<Alert
key={index}
kind={kinds.DANGER}
>
{error.errorMessage}
</Alert>
);
})
}
{
validationErrors.length || validationWarnings.length ?
<div className={styles.validationFailures}>
{
validationErrors.map((error, index) => {
return (
<Alert
key={index}
kind={kinds.DANGER}
>
{error.errorMessage}
</Alert>
);
})
}
{
validationWarnings.map((warning, index) => {
return (
<Alert
key={index}
kind={kinds.WARNING}
>
{warning.errorMessage}
</Alert>
);
})
}
</div>
{
validationWarnings.map((warning, index) => {
return (
<Alert
key={index}
kind={kinds.WARNING}
>
{warning.errorMessage}
</Alert>
);
})
}
</div> :
null
}
{children}
</div>

View File

@@ -1,5 +1,5 @@
.button {
composes: button from 'Components/Link/Button.css';
composes: button from '~Components/Link/Button.css';
border-left: none;
border-top-left-radius: 0;

View File

@@ -1,5 +1,6 @@
.inputGroupContainer {
flex: 1 1 auto;
min-width: 0;
}
.inputGroup {
@@ -11,6 +12,7 @@
.inputContainer {
position: relative;
flex: 1 1 auto;
min-width: 0;
}
.inputUnit {

View File

@@ -16,7 +16,7 @@ import QualityProfileSelectInputConnector from './QualityProfileSelectInputConne
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import SelectInput from './SelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
import TagInputConnector from './TagInputConnector';
import TextTagInputConnector from './TextTagInputConnector';
import TextInput from './TextInput';
@@ -65,7 +65,7 @@ function getComponent(type) {
return RootFolderSelectInputConnector;
case inputTypes.SELECT:
return SelectInput;
return EnhancedSelectInput;
case inputTypes.SERIES_TYPE_SELECT:
return SeriesTypeSelectInput;

View File

@@ -33,7 +33,7 @@
}
.link {
composes: link from 'Components/Link/Link.css';
composes: link from '~Components/Link/Link.css';
margin-left: 5px;
}

View File

@@ -0,0 +1,23 @@
.optionText {
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 0 0;
min-width: 0;
&.isMobile {
display: block;
.hintText {
margin-left: 0;
}
}
}
.hintText {
@add-mixin truncate;
margin-left: 15px;
color: $darkGray;
font-size: $smallFontSize;
}

View File

@@ -0,0 +1,44 @@
import PropTypes from 'prop-types';
import React from 'react';
import classNames from 'classnames';
import EnhancedSelectInputOption from './EnhancedSelectInputOption';
import styles from './HintedSelectInputOption.css';
function HintedSelectInputOption(props) {
const {
value,
hint,
isMobile,
...otherProps
} = props;
return (
<EnhancedSelectInputOption
isMobile={isMobile}
{...otherProps}
>
<div className={classNames(
styles.optionText,
isMobile && styles.isMobile
)}
>
<div>{value}</div>
{
hint != null &&
<div className={styles.hintText}>
{hint}
</div>
}
</div>
</EnhancedSelectInputOption>
);
}
HintedSelectInputOption.propTypes = {
value: PropTypes.string.isRequired,
hint: PropTypes.node,
isMobile: PropTypes.bool.isRequired
};
export default HintedSelectInputOption;

View File

@@ -0,0 +1,24 @@
.selectedValue {
composes: selectedValue from '~./EnhancedSelectInputSelectedValue.css';
display: flex;
align-items: center;
justify-content: space-between;
overflow: hidden;
}
.valueText {
@add-mixin truncate;
flex: 0 0 auto;
}
.hintText {
@add-mixin truncate;
flex: 1 10 0;
margin-left: 15px;
color: $gray;
text-align: right;
font-size: $smallFontSize;
}

View File

@@ -0,0 +1,43 @@
import PropTypes from 'prop-types';
import React from 'react';
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
import styles from './HintedSelectInputSelectedValue.css';
function HintedSelectInputSelectedValue(props) {
const {
value,
hint,
includeHint,
...otherProps
} = props;
return (
<EnhancedSelectInputSelectedValue
className={styles.selectedValue}
{...otherProps}
>
<div className={styles.valueText}>
{value}
</div>
{
hint != null && includeHint &&
<div className={styles.hintText}>
{hint}
</div>
}
</EnhancedSelectInputSelectedValue>
);
}
HintedSelectInputSelectedValue.propTypes = {
value: PropTypes.string,
hint: PropTypes.string,
includeHint: PropTypes.bool.isRequired
};
HintedSelectInputSelectedValue.defaultProps = {
includeHint: true
};
export default HintedSelectInputSelectedValue;

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