1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-30 18:24:22 -04:00

Compare commits

..

408 Commits

Author SHA1 Message Date
Taloth Saldono
c0a961bb94 New: Updated TheXEM server url on xem admin request 2021-09-28 22:21:36 +02:00
Mark McDowall
6994ca720a Fixed: Parsing multi-episode file name with number in episode title
Closes #4613
2021-09-05 21:45:05 -07:00
Mark McDowall
1d8b711eda Aria2 fixes
Fixed: Removing completed downloads from Aria2
Fixed: Return correct path for Aria2 downloads in a job folder
Fixed: Seeding torrents in Aria2 are treated as finished downloading
Closes #4648
2021-09-03 21:41:52 -07:00
Mark McDowall
77fdebc366 Do not unmonitor episodes after using Manual File Import 2021-08-30 17:29:13 -07:00
Mark McDowall
dd3899806b Fixed: Parsing of some anime with standard numbers
Closes #4573
2021-08-29 20:40:46 -07:00
Mark McDowall
574f05e296 Fixed: Parsing of some multi-episode titles
Closes #4612
2021-08-29 20:15:27 -07:00
Mark McDowall
5c4687e0d9 Fixed: Unmonitor episodes after using Manual File Import
Closes #4638
2021-08-29 19:54:05 -07:00
Mark McDowall
e19d4cf85b Fixed: Log active indexers instead of implying all indexers are searched
Closes #4642
2021-08-29 19:25:41 -07:00
Mark McDowall
6b84da614b Fixed: Queue conflicts with the same download in multiple clients 2021-08-29 19:23:48 -07:00
LoV432
31833253dd Fixed: Erai-Raws Batches Parsing 2021-08-29 19:13:49 -07:00
Qstick
dbd140d4ec Update QualityDefinitionsConnector.js 2021-08-20 21:02:09 -07:00
Qstick
e9a49941c9 Fixed: Help message when adding download clients 2021-08-20 21:01:43 -07:00
Qstick
0fe436b952 Aria2 formatting cleanup 2021-08-20 21:01:23 -07:00
Qstick
22f044844c Fix Phantom branch reference in Join proxy 2021-08-15 20:14:51 +02:00
Taloth Saldono
20306a38e1 Fixed: Release Push api broken when no indexer id is specified 2021-08-11 13:48:12 +02:00
Winter
0d03dba6ea Fix Feature Request issue template 2021-08-07 09:22:19 +02:00
Winter
1c6863dd27 Fix Bug Report issue template
The `title` key is optional, but must not be empty if specified.
2021-08-07 09:22:19 +02:00
Mark McDowall
3f60e28c42 Fixed: Blocklisting pending releases
Closes #4598
2021-08-05 17:17:40 -07:00
Robin Dadswell
ead1371846 New: Renamed Blacklist to Blocklist 2021-08-05 16:55:23 -07:00
Taloth Saldono
2f6409226a Fixed: Cleanup of unused tags for Import lists.
Fixes #4610
2021-08-06 01:54:12 +02:00
bakerboy448
3fb5f65f08 Use YML Github templates 2021-08-05 16:16:57 -07:00
Mark McDowall
38feeefea3 Fixed: Improve More Info links for Indexers, download clients, etc 2021-08-03 19:13:08 -07:00
Mark McDowall
a7a3c546e5 Fix broken test 2021-08-03 18:55:45 -07:00
Taloth Saldono
f107ea5678 Simplified regex a bit. 2021-08-03 21:59:30 +02:00
Taloth Saldono
59409a7e72 Fixed: Parse endpoint response when title failed to parse
Closes #4591
2021-08-03 20:55:27 +02:00
Mark McDowall
dc7f46027a Fixed: Prevent conflicts with reserved device names
Closes #4595
2021-08-01 16:45:23 -07:00
Mark McDowall
2031da05f6 Updated more wiki links 2021-08-01 16:12:30 -07:00
Mark McDowall
92b9f46399 Fixed HealthCheckFixture test 2021-08-01 16:05:36 -07:00
Mark McDowall
0a30735f34 Fixed: Updated supported wiki links 2021-08-01 16:00:52 -07:00
Mark McDowall
021fd4afa7 Fixed: Updated wiki links 2021-08-01 15:34:44 -07:00
siankatabg
57e3bd8b4d New: Bulgarian Language 2021-08-01 13:32:49 -07:00
bakerboy448
3bbec2ff5d Fixed: Incorrectly Parsing [PublicHD] as Release Group 2021-08-01 13:30:33 -07:00
Zippy79
155dbd4dd5 New: Add TVDB URL in Kodi metadata 2021-08-01 13:29:25 -07:00
Mark McDowall
4bf3ab1511 Improve default path for Synology Download Station
Fixes: Missing default path for Download Station
Fixes: Error when getting destination path for Synology Download Station in health check
Closes #4562
2021-08-01 13:25:50 -07:00
LLeny
6fd31613c2 New: Aria2 Torrent Client
Closes #1374
2021-08-01 12:49:36 -07:00
Alanoll
d4cd4a9549 New: Named Release Profile preferred word renaming tokens 2021-08-01 12:45:26 -07:00
Mark McDowall
6596d0b4da Fixed: Show error if adding root folder fails
Closes #4570
2021-08-01 12:09:02 -07:00
Mark McDowall
dca2cfcecd Fixed: Peers filtering in Interactive Search results
Closes #4583
2021-08-01 12:09:01 -07:00
Mark McDowall
076c293942 Fixed: Monitoring pilot episode will not monitor first season
Closes #4597
2021-08-01 12:09:01 -07:00
Alex Thomson
94417402d8 Remove duplicate call to DeleteTorrent 2021-07-27 16:17:04 +02:00
bakerboy448
4659a8366d Update bug report template 2021-07-13 10:23:31 -07:00
6cUbi57z
c3d54b312e New: Add tag support to indexers
Closes #487
2021-07-04 11:17:57 -07:00
Yukine
14b551b027 Fixed: Parsing of some anime releases with year in title 2021-07-04 10:19:55 -07:00
Nathaniel Peiffer
43cd103248 New: Add Size column to Activity: Queue
Closes #4527
2021-07-04 10:14:42 -07:00
bakerboy448
bd4624c0ab Add wiki link for logs in bug report template 2021-07-04 10:13:20 -07:00
bakerboy448
b9539cc1f7 Update Contributing.md 2021-06-22 07:46:47 -07:00
Minamiya Natsuki
fc8bbf29d1 Fixed: Parsing of some Chinese anime releases without brackets 2021-06-21 20:42:32 -07:00
Robin Dadswell
98e5442f24 New: Added Running Years into the shows details 2021-06-21 20:39:10 -07:00
bakerboy448
c30ce3580a Fixed: Incorrectly parsing RePACKPOST as Group 2021-06-21 20:38:50 -07:00
Nathaniel Peiffer
2ddf131e1a New: Activity Queue: Rename Timeleft column to Time Left 2021-06-21 20:37:00 -07:00
Jake Soenneker
2f366bc3b7 New: Manual Import rejection column is sortable 2021-06-21 20:35:23 -07:00
TwentyNine78
49e90463e5 Fixed: Compatibility with the new Download Station API
Fixes #4388
2021-06-21 20:29:50 -07:00
Mark McDowall
4402f475d4 Update IRC links to Libera.chat 2021-06-17 11:26:49 -07:00
Qstick
6db2401359 Fixed: Use normal URL for Trakt Oauth per new docs
Closes #4521
2021-06-08 07:33:17 -07:00
Taloth Saldono
690b851836 Reverted test code 2021-05-27 10:15:52 +02:00
Mark McDowall
2804a961cb Fix webpack memory leak when copying HTML files 2021-05-26 21:34:03 -07:00
Taloth Saldono
32f2d417d3 Added EpisodesChanged to signalR series event to notify that episode monitored flag may have changed 2021-05-26 21:28:30 +02:00
Evan J
e8f58eb9be Update login.html 2021-05-24 17:21:29 -07:00
Robin Dadswell
a22a38016d Fixed: Root Folder Downloads check giving errors when RuTorrent is used 2021-05-24 17:19:17 -07:00
Taloth Saldono
6c5cc430b7 Clarify delay profile bypass only applies to preferred protocol 2021-05-22 10:44:57 +02:00
Taloth Saldono
9e81d41f26 Send signalr message for episode monitored flag changes 2021-05-22 10:40:10 +02:00
Taloth Saldono
dab1834960 Fixed: Inversion of defaults in CDH migration. 2021-05-20 01:44:11 +02:00
Mark McDowall
c3837c9f7b Fix ESLint 2021-05-19 15:06:06 -07:00
Qstick
2ad4e21aad Fixed: NZBGet Settings hint mentions Sabnzbd
closes #4494
2021-05-18 09:45:33 +02:00
Jesse Chan
707c2c7978 Flood: explicitly cast DateFinished long? to long 2021-05-18 09:44:39 +02:00
Jesse Chan
c8ad01e38e New: Removing Flood downloads when seeding criteria have been met
closes #4492
2021-05-17 22:26:58 +02:00
Taloth Saldono
fe8f319f7b Show User Agent in System->Tasks for externally triggered commands 2021-05-16 12:43:17 +02:00
Taloth Saldono
91fe47ef31 Removed extraneous enum hints in dropdown 2021-05-16 00:48:14 +02:00
Taloth Saldono
2dba5ef4b4 New: Per download client setting to Remove Completed/Failed downloads instead of global setting 2021-05-16 00:48:14 +02:00
Taloth Saldono
8e46362ff9 Fixed SeedConfigProvider cache refresh after indexer settings change 2021-05-16 00:48:14 +02:00
leaty
411be4d011 New: Removing rtorrent downloads when seeding criteria have been met 2021-05-16 00:48:14 +02:00
Robin Dadswell
cad8b37df8 New: Health Check for Downloads to Root Folder
already implemented within Radarr
(cherry picked from commit 88780f33a4c5032aaa151aaae7090371beb42f33)
2021-05-15 15:00:04 -07:00
Michael Higgins
82e0a4bd0e New: Setting SceneName and ReleaseGroup for EpisodeFiles via API 2021-05-15 14:58:53 -07:00
Taloth Saldono
ee227e3b1d Fixed timing issues
fixes #4487
2021-05-12 23:56:14 +02:00
Mark McDowall
ff048ffcb8 Spaces... 2021-05-10 20:43:38 -07:00
Mark McDowall
93443f1db2 Fixed: Adding new series via API with seasons array missing 2021-05-10 20:35:30 -07:00
Mark McDowall
a2427bd9df Fixed: Parsing of 540p releases 2021-05-10 20:35:30 -07:00
Mark McDowall
c722e91124 New: Add rel="noreferrer" to all external links 2021-05-10 20:35:30 -07:00
Mark McDowall
05bee7c37a Fixed: Remove preview from site name 2021-05-10 20:35:30 -07:00
Taloth Saldono
5dff21e6d5 Fixed logger parameter 2021-05-09 01:54:22 +02:00
Taloth Saldono
b3dbff1642 Log when season,ep query parameters aren't supported. 2021-05-09 01:10:09 +02:00
Taloth Saldono
8fc68420c9 Fixed: Scene Info displaying mappings for wrong season
Fixed: Extraneous searches based on scene mappings
2021-05-08 01:03:47 +02:00
Taloth Saldono
87897d56ea Fixed: MediaInfo tag in renaming format for certain OS language cultures
Based-On: a0d2af54e8
2021-05-07 22:09:23 +02:00
Taloth Saldono
bcdbadbede Simplify release titles before applying scene naming exception regex filter 2021-05-07 20:17:05 +02:00
Taloth Saldono
87f10bf7bb Fixed: TheXEM mappings not used properly when parsing season packs 2021-05-07 20:16:09 +02:00
Mark McDowall
a8a9399469 Fixed: Parsing of some anime releases with numbers in title 2021-04-25 02:22:10 -07:00
Mark McDowall
5d316ad7dc Fixed scrolling via jump list on series index 2021-04-24 21:57:54 -07:00
Mark McDowall
56a33e3b4c Use webpack environment variables in build.sh 2021-04-24 19:25:02 -07:00
Mark McDowall
684626ef73 Updated react-dnd and added touch support
Fixed: Drag and drop on mobile devices
Closes ##4429
2021-04-24 19:25:02 -07:00
Mark McDowall
47f3886d7a Updated react-virtualized 2021-04-24 19:25:02 -07:00
Mark McDowall
8acc0f77b6 Updated create-react-class package 2021-04-24 19:25:01 -07:00
Mark McDowall
23a7c728f4 Updated redux and react-redux packages 2021-04-24 19:25:01 -07:00
Mark McDowall
2964d0bb6d Update react-autosuggest, react-focus-lock, react-lazyload react-slider and react-tabs packages 2021-04-24 19:25:01 -07:00
Mark McDowall
c92d25694f Updated react-addons-shallow-compare package 2021-04-24 19:25:01 -07:00
Mark McDowall
a831ece57d Updated react and react-dom packages 2021-04-24 19:25:01 -07:00
Mark McDowall
b4973fd99d Updated jquery, filesize and qs packages 2021-04-24 19:25:01 -07:00
Mark McDowall
a0e3afd614 Updated clipboard, fuse.js, mobile-detect, moment and signalr packages 2021-04-24 19:25:01 -07:00
Mark McDowall
91c895219f Update classnames 2021-04-24 19:25:01 -07:00
Mark McDowall
38a312895a Update sentry 2021-04-24 19:25:01 -07:00
Mark McDowall
44cb493847 Update fontawesome 2021-04-24 19:25:01 -07:00
Mark McDowall
30ed105afc Update postcss packages 2021-04-24 19:25:00 -07:00
Mark McDowall
0374f05743 Upgrade core-js package 2021-04-24 19:25:00 -07:00
Mark McDowall
023498fdb9 Update linter packages 2021-04-24 19:25:00 -07:00
Mark McDowall
36d4c2c9a0 Upgrade babel packages 2021-04-24 19:25:00 -07:00
Mark McDowall
dde53b12a8 Removed gulp and organized package.json 2021-04-24 19:25:00 -07:00
Mark McDowall
a3bb2f1c32 Fixed files that were using incorrect imports 2021-04-24 19:25:00 -07:00
Mark McDowall
bd83a2af88 Forward slashes are actually forward slashes 2021-04-24 19:20:35 -07:00
bakerboy448
44f9d45938 New: Date format in log files 2021-04-24 19:17:33 -07:00
Robin Dadswell
a824fa44d2 Add missing On Delete Notifications to Mailgun notifications 2021-04-23 07:54:15 -07:00
cicomalieran
8175f19442 Fixed: Parsing RSS with null values 2021-04-22 16:38:26 -07:00
Skyler Mäntysaari
55752a6c62 New: Mailgun connection 2021-04-22 16:37:28 -07:00
bakerboy448
8d2d9078ff Update indexer category help text 2021-04-22 16:36:39 -07:00
Zippy79
199b126a46 New: Adds SSL option to Kodi connections 2021-04-22 16:36:05 -07:00
Mark McDowall
16156192c5 Fixed broken tests 2021-04-21 16:26:11 -07:00
Mark McDowall
0a2b109a3f Fixed: Refresh queue count when navigating Activity: Queue
Closes #4446
2021-04-20 00:21:20 -07:00
Mark McDowall
fde28c63d9 New: Removed EpisodeGuide tag from Kodi Metadata
Closes #4448
2021-04-20 00:11:09 -07:00
Mark McDowall
4ffa81c783 Fixed: Parsing WEB quality from some anime releases 2021-04-20 00:05:54 -07:00
Mark McDowall
6934eafd44 Fixed: Round durationseconds in Kodi metadata 2021-04-20 00:05:54 -07:00
Mark McDowall
c669be317f Fixed: Removing completed download from SABnzbd
Closes #4445
2021-04-20 00:05:54 -07:00
Mark McDowall
6079f1ef11 Fixed: CRC32 being parsed as release group
Closes #4449
2021-04-20 00:05:54 -07:00
Mark McDowall
3f8bb24b75 Fixed: Don't ignore number only aliases 2021-04-20 00:05:54 -07:00
Mark McDowall
dd8fbb438f Fixed: Parsing of some anime releases 2021-04-20 00:05:54 -07:00
Mark McDowall
d29e254dcb New: File info scrolls on mobile
Closes #4436
2021-04-12 22:15:59 -07:00
Mark McDowall
5938c38bc3 Fixed: Custom Script Health Issue Level 2021-04-12 22:15:27 -07:00
Mark McDowall
a47cb2390e Fixed: Links to Sonarr now uses auth cookie
Closes #4440
2021-04-12 22:14:41 -07:00
Mark McDowall
5aae777a18 Obfuscate anime title 2021-04-10 22:49:38 -07:00
Mark McDowall
5114c75cbb Fixed: Incorrectly grabbing revision downgrades
Closes #4431
2021-04-10 14:59:28 -07:00
Mark McDowall
d7e9ccde8e Fixed: Parsing of anime titles with numbers in the middle of their names
Closes #4433
2021-04-10 14:38:33 -07:00
Mark McDowall
100b87193d Fixed: Background click not closing episode modal when first opened 2021-04-10 01:36:43 -07:00
Mark McDowall
5449389ca5 Set timeout for sending email
Closes #4348
2021-04-09 10:38:16 -07:00
Mark McDowall
7da695bd62 Revert "Handle events asynchronously for notifications"
This reverts commit 46da657740.
2021-04-09 10:36:56 -07:00
Mark McDowall
46da657740 Handle events asynchronously for notifications
Fixed: Slow notification sending slowing down imports
Closes #4348
2021-04-08 17:36:24 -07:00
Mark McDowall
5301620ecf Refactor Email and add more logging 2021-04-08 17:34:56 -07:00
Mark McDowall
77ac0bb44c Fixed: Importing a language upgrade with a worse preferred word score 2021-04-08 17:27:01 -07:00
Mark McDowall
75e9d33fea Confirm scene name is not folder name for batch releases 2021-04-07 18:03:22 -07:00
Mark McDowall
ac8283d733 New: Remove completed downloads from disk when removing from SABnzbd
Closes #4423
2021-04-07 18:03:21 -07:00
Mark McDowall
f2422f814d Fixed: Parsing of some 360p multi-episode files 2021-04-07 18:03:21 -07:00
Mark McDowall
cb8a29ec00 Fixed: Some connection settings being invisibly enabled on creation 2021-04-07 18:03:21 -07:00
Mark McDowall
a4dea0aa62 Fix spelling of separated 2021-04-07 18:03:21 -07:00
Daniel Martin Gonzalez
d6e5d9c424 New: Add option to import from any user's personal list 2021-04-05 15:45:23 -07:00
Mark McDowall
1e99856ffc Fixed: Exception when searching some anime
Closes #4414
2021-03-29 13:27:52 -07:00
Mark McDowall
2da2420415 New: Series Year Renaming token
Closes #4407
2021-03-29 13:27:52 -07:00
Mark McDowall
0210b5c5c1 New: Calendar option for full color events 2021-03-29 13:27:52 -07:00
Mark McDowall
66c805feaf New: Support "Series" instead of "Season" in season packs
Closes #4399
2021-03-29 13:27:52 -07:00
Taloth Saldono
a200dd5f6d Bump package version 2021-03-27 22:14:03 +01:00
Taloth Saldono
f57efd30b8 Fixed data dir ownership in case of dpkg-reconfigure 2021-03-27 21:30:30 +01:00
bakerboy448
f2f1039c5e Fixed: Debatable typos in Naming Modal 2021-03-27 13:30:22 -07:00
bakerboy448
12ba4f73ed Updating the bug template yet again 2021-03-26 19:14:08 -07:00
Mark McDowall
370280b4bf Another wiki URL update
Close #4411
2021-03-26 19:08:04 -07:00
Mark McDowall
6c505937da Fixed: Interactive import modal horizontal scrolling on Firefox mobile
Closes #4401
2021-03-23 20:37:13 -07:00
Taloth Saldono
7272c5b7fc Added IsTorrentLoaded to tests 2021-03-20 00:33:51 +01:00
Mark McDowall
1d06c3fc15 Revert vswhere command 2021-03-19 15:21:04 -07:00
Mark McDowall
ec9f62285a Updated vswhere.exe 2021-03-19 14:40:17 -07:00
Mark McDowall
aae0d1c4ba Updated nuget.exe 2021-03-19 14:25:59 -07:00
Taloth Saldono
652d44722b Fixed: Qbittorrent api errors when only one of two seed criteria was configured
closes #4393
2021-03-19 21:32:42 +01:00
Taloth Saldono
5a69801877 Fixed: Unnecessary idle cpu usage
ref #4386
2021-03-19 02:48:09 +01:00
Taloth Saldono
fa8b2f48e7 Don't ignore original wal/journal during v3 migration 2021-03-18 23:43:16 +01:00
Taloth Saldono
34faa417c1 Fixed: Database migration failure when database was manually repaired in a certain way
fixes #4390
2021-03-18 01:25:21 +01:00
Mark McDowall
d6c0635a26 New: Improve message if Sonarr can't bind to IP/port during startup
Closes #4352
2021-03-17 17:21:30 -07:00
Mark McDowall
d4167d7169 New: Support for using parsed season number for some anime releases without aliases
Closes #4377
2021-03-17 17:14:54 -07:00
Taloth Saldono
eea6be459d Fixed post-install update check not running 2021-03-14 20:24:08 +01:00
Taloth Saldono
67e97f7aee Fixed: Setting seed criteria while torrent is still being loaded by qbittorrent
closes #4360
2021-03-14 00:46:28 +01:00
Mark McDowall
37e1c4f2eb New: Don't close interactive search with background click 2021-03-11 08:23:06 -08:00
Taloth Saldono
a9b8ec3505 Fixed failing tests 2021-03-10 23:38:59 +01:00
Taloth Saldono
f57cf1561b Log Skyhook connection failures with more info. 2021-03-10 23:05:19 +01:00
Taloth Saldono
6672650b6b Log Skyhook connection failures with more info. 2021-03-10 21:44:32 +01:00
Taloth Saldono
e4a064a1c0 Added comment to sonarr.service 2021-03-10 21:44:32 +01:00
Taloth Saldono
a848e575cd Make it clearer that Maximum size is the global limit. 2021-03-10 21:44:32 +01:00
Taloth Saldono
01995e686d New: Multiple Recipients on Email Notifications (Also CC, BCC)
Based on Qstick's Radarr commit of the same name
closes #4369

Signed-off-by: Taloth Saldono <Taloth@users.noreply.github.com>
2021-03-10 21:44:31 +01:00
Taloth Saldono
32058f1705 Fixed systemd unit search&replace issue and added umask to debconf 2021-03-10 21:44:31 +01:00
Mark McDowall
af3696af08 On Download -> On Import (again) 2021-03-10 11:25:29 -08:00
Mark McDowall
1477356cfc Update Discord link 2021-03-08 19:29:11 -08:00
Mark McDowall
aa19ddfbfd Fixed: Parsing of absolute episode numbers over 1000
Closes #4367
2021-03-08 19:22:41 -08:00
Mark McDowall
a697a69e88 Fixed: Some health check wiki links 2021-03-08 19:22:34 -08:00
Mark McDowall
3abb7e156a Fixed: Parsing of absolute episode number inside square brackets
Closes #4331
2021-03-07 17:31:43 -08:00
Mark McDowall
240791a7cd Fixed: Parsing of anime batch releases using a tilde instead of a dash
Closes #4330
2021-03-07 17:10:00 -08:00
Mark McDowall
0fe2453962 Fixed: Parsing similar series titles with common words at end 2021-03-07 16:53:56 -08:00
Robin Dadswell
85f4cbe94c Fix: Consistent SSL option for Download Clients
Closes #4323
2021-03-07 16:32:19 -08:00
Mark McDowall
e1f7bce14b New: Simplify Connection trigger settings
Closes #4351
2021-03-07 16:24:20 -08:00
Mark McDowall
675c72f02e Fixed: Set SameSite=Strict for SonarrAuth cookie
Closes #4365
2021-03-07 16:24:20 -08:00
Mark McDowall
6619350f87 Fixed: Don't set cookies for static resources
Closes #4356
2021-03-07 16:24:20 -08:00
Mark McDowall
efd9fe9ad0 Fixed: Cache headers for static resources
Towards #4356
2021-03-07 16:24:20 -08:00
Mark McDowall
ab502ffda4 Just one Application Version header 2021-03-07 16:24:20 -08:00
Taloth Saldono
90697d77a5 Bumped package version for main 2021-03-07 23:27:22 +01:00
Qstick
fa7aa05d60 Cleanup formatting in PackageGlobalMessageCheck.cs 2021-03-07 13:26:19 -08:00
Mark McDowall
4ed5fefcc6 Fixed: Remove selected in queue
Closes #4364
2021-03-07 12:24:06 -08:00
Taloth Saldono
4c324fbbbf Fixed failing test 2021-03-07 00:00:41 +01:00
Taloth Saldono
7da02c236a Added mechanism for package maintainers to produce a health check error. 2021-03-06 22:47:15 +01:00
Taloth Saldono
79cfa3a5f6 Removed bad file from commit 2021-03-06 22:47:15 +01:00
Qstick
2746556ae2 Cleanup trailing space in HttpResponse 2021-03-06 17:49:14 +01:00
Taloth Saldono
d668e923af New: Allow user to choose whether delay profile should apply to release of the highest enabled quality 2021-03-06 13:57:11 +01:00
Taloth Saldono
24ca47356e Sentry logging exceptions and some trace logging 2021-03-06 13:57:11 +01:00
Taloth Saldono
ab4f57f2fa Debug logging for email notifications
ref #4348
2021-03-06 13:57:10 +01:00
Mark McDowall
13ff2d4c70 Fixed: Restoring a backup with a different API didn't reload properly 2021-03-05 18:29:00 -08:00
Mark McDowall
2728bf79ca New: Improve messaging if release is in queue because all episodes in release were not imported 2021-03-05 18:29:00 -08:00
Mark McDowall
cd28af98ee Fixed: Removal of previous service 2021-03-05 18:28:59 -08:00
Mark McDowall
e9818b9982 Fixed: Queue refresh closing manual import from queue if items change 2021-03-05 18:28:59 -08:00
Qstick
d6cf370bcd Handle 303 and 307 redirects in Http Requests 2021-03-03 20:43:44 -08:00
Michael Casey
cb8ed74fe9 New: Add Recommended to the List types for Trakt
Closes #4167
2021-02-27 12:18:07 -08:00
bakerboy448
4e81b33006 Update contributing.md Github docs URL 2021-02-27 12:16:38 -08:00
bakerboy448
e67864fecb Fixed: Cleanse Tracker Announce Keys from logs
Closes #4341
2021-02-25 00:22:58 +01:00
Taloth Saldono
e289c428c6 Fixed: Refresh scene naming exceptions on series add to help first-use scenario 2021-02-20 20:04:34 +01:00
Taloth Saldono
23047623ee Cleanse more /home/username scenarios 2021-02-20 20:04:34 +01:00
Mark McDowall
062e47e27e Fixed: History details incorrect when preferred word score was 0
Closes #4328
2021-02-16 22:35:11 -08:00
Taloth Saldono
28ba037630 Fixed: Searching specials with NNTMux-based usenet indexers 2021-02-16 21:57:53 +01:00
Taloth Saldono
82da38941e Fixed: Debian package dependencies
closes #4332
2021-02-16 21:57:53 +01:00
Mark McDowall
10c770b116 Fixed: Use original file path when calculating preferred word score for existing file
Closes #3488
Closes #3913
2021-02-13 17:13:09 -08:00
Mark McDowall
3c45349404 New: Include renamed file information for Webhook and Custom Scripts
Closes #3927
2021-02-13 17:13:09 -08:00
Mark McDowall
b815d27a10 New: Include episode file with episode file deleted events
Closes #4282
2021-02-12 17:01:31 -08:00
Mark McDowall
ec698c2cf7 Fixed: Parsing of release names with trailing colon in the title
Closes #4238
2021-02-11 17:00:11 -08:00
Mark McDowall
e7d57a95f2 Series editor column fixes
Fixed: Series Editor sorting by size on disk
Fixed: Series Editor column order/enabled lost on refresh
2021-02-10 16:52:21 -08:00
Mark McDowall
1250d71e80 Appeasing the lint gods 2021-02-09 20:17:53 -08:00
Mark McDowall
e42d1af5ff Fixed: Unable to close indexer category select input on mobile
Closes #4296
2021-02-09 17:35:32 -08:00
Mark McDowall
88ad6f9544 Fixed: Error checking if files should be deleted after import won't leave import in limbo
Closes #4318
2021-02-09 17:35:32 -08:00
Michael Casey
54c386dd22 Use SVG for loading page icon
closes #4311
2021-02-09 19:20:59 +01:00
Mark McDowall
694360457d Fixed: Error logged when notification fails to send after episode file is deleted
Closes #4289
2021-02-09 07:55:25 -08:00
Mark McDowall
ae196af2ad New: Health check for import lists with missing root folders
New: Show missing root folder path in edit for Import List

Closes #4315
2021-02-08 23:12:23 -08:00
Mark McDowall
12fafb2457 Fixed: Mark as Failed errors 2021-02-08 19:26:06 -08:00
Mark McDowall
795bc91d25 Fixed: Error logged when notification fails to send after episode file is deleted
Closes #4289
2021-02-08 16:39:47 -08:00
Mark McDowall
044342f677 Fixed: Scene name not being set during import 2021-02-08 11:10:42 -08:00
Mark McDowall
5960035d5d Fixed: Restoring backup from zip file on disk 2021-02-08 08:21:14 -08:00
Mark McDowall
6c324c8a1c Fixed: Errors loading queue after episodes in series are removed
Closes #3565
2021-02-07 20:25:44 -08:00
Mark McDowall
54a267d860 Fixed: Don't automatically import if absolutely numbered file if it doesn't match expected season
Closes #377
2021-02-07 17:28:16 -08:00
Mark McDowall
b5f08a8f06 Alternate titles prop validation 2021-02-07 16:52:57 -08:00
Mark McDowall
653db8290e Update column properties when restoring persisted state 2021-02-07 16:52:57 -08:00
Mark McDowall
cbc4295f28 Fixed: Use file name when importing batch release when renaming is disabled
Closes #3056
2021-02-07 16:52:57 -08:00
Mark McDowall
8876c9194d New: Show preferred word score in history 2021-02-07 16:52:56 -08:00
Taloth Saldono
d898f55660 Generalized RateLimit logic to all indexers based on indexer id 2021-02-08 00:09:59 +01:00
Taloth Saldono
a85979c2f6 New: Added Hindi language
closes #4275
2021-02-07 21:35:30 +01:00
bakerboy448
ae63373b2b Update parser tests to be generic 2021-02-07 21:06:41 +01:00
Mark McDowall
044cb563a6 Fixed: Table column order resetting after refresh
#4297
2021-02-07 11:52:44 -08:00
Michael Casey
5302ee05bc New: Add logo to loading page
Closes #4304
2021-02-07 11:51:53 -08:00
Taloth Saldono
29bc660cfb Fixed: Jackett indexer search performance 2021-02-07 19:50:04 +01:00
Taloth Saldono
f8b8afdaa2 New: Added Arabic language 2021-02-07 19:50:04 +01:00
Ricardo Loureiro
33b708927c Fixed: Authentication on DSM 7
closes #3943
ref #4313
2021-02-07 14:05:31 +01:00
Mark McDowall
42d9e37e7d Fixed: Settings fields being altered during save
Closes #4309
2021-02-06 19:28:41 -08:00
Michael Casey
fc3bea370f New: Persist search settings in add new series
Closes #4245
2021-02-06 19:18:47 -08:00
Robin Dadswell
a1ddcf2b7b New: Show number of files as tooltip over size on disk
Closes #4203
2021-02-06 19:15:31 -08:00
bakerboy448
5b98a17873 Update feature request template 2021-02-06 19:06:53 -08:00
Mark McDowall
8fd4adbdb6 New: use-credentials for maniftest requests
Closes #4305
2021-02-04 20:28:24 -08:00
Mark McDowall
952a7248c9 New: Add FileId to History data for import events 2021-02-04 20:28:24 -08:00
bakerboy448
577604fccc Fixed: Series Removed From TVDB wiki link 2021-02-03 20:50:43 -08:00
Matt Evans
3d3cd8cf5c Detect Dolby Vision as HDR and MediaInfo Update
Fixed: Detect Dolby Vision as HDR
New: Updated MediaInfo on Windows and MacOS
Closes #4276
2021-02-02 17:19:22 -08:00
Robert Dailey
5e4c9dfe60 New: Add name field to release profiles 2021-02-02 16:51:06 -08:00
Taloth Saldono
f3f2648ce5 Fixed: Global scene mapping aliases disappeared from UI 2021-02-01 16:52:34 +01:00
Taloth Saldono
d1c3ae1749 Fixed: Validation of new qbittorrent max-ratio action config 2021-02-01 16:52:32 +01:00
Taloth Saldono
1cbcad6960 Added searchEngine support in Newznab/Torznab caps 2021-02-01 16:52:31 +01:00
Taloth Saldono
ab45910e56 Fixed: FLAC audio channels in media info 2021-02-01 16:52:30 +01:00
Mark McDowall
53f5694535 New: Disable season search if series is unmonitored 2021-01-31 11:04:55 -08:00
bakerboy448
7a3f4e8033 Fixed: Handle more obfuscated names
Closes #4198
2021-01-31 10:53:26 -08:00
Robin Dadswell
474f4bcc6d New: On Episode File Delete For Upgrade notification option 2021-01-30 14:54:26 -08:00
Robin Dadswell
ec058436d3 New: Unify series custom filter options
Closes #3548
2021-01-30 14:34:20 -08:00
Mark McDowall
39ca348666 Fixed: Label for 'On Episode File Delete' 2021-01-24 13:21:16 -08:00
Mark McDowall
02a46349a2 Consistent types for on delete custom script events 2021-01-24 12:19:15 -08:00
Mark McDowall
98dc20d919 Fixed: Webhook events not sent for series deletions 2021-01-24 12:18:39 -08:00
Mark McDowall
a510c9e86d Separate event types for series and episode deletions 2021-01-24 12:17:01 -08:00
Mark McDowall
f5d690aa7b Fixed: Queue refresh closing manual import from queue if items change
Closes #4221
2021-01-24 10:14:45 -08:00
Robin Dadswell
c91fabcf2d New: On Delete Notifications
Closes #2410
2021-01-24 01:21:29 -08:00
Mark McDowall
21fafb895f Fixed: Files with lower preferred word scores are imported
Closes #4212
2021-01-24 01:18:08 -08:00
Mark McDowall
8047e5aa67 Fixed: Series Type Filter
Closes #4274
2021-01-23 19:15:36 -08:00
Mark McDowall
546d65b663 Manual Import episode improvements
New: Show absolute episode number (for anime series) and title in manual import
New: Show absolute episode number (for anime series) in manual import episode selection
2021-01-23 19:02:22 -08:00
Mark McDowall
328cfa12f6 Fixed: Improve multi-episode title squashing 2021-01-23 18:16:32 -08:00
bakerboy448
d475fccb55 Update bug report template 2021-01-20 23:48:00 -08:00
bakerboy448
50a2e52c19 Lock closed issues after 90 days without activity 2021-01-20 23:47:19 -08:00
Jesse Chan
28e0ad38b0 New: Flood Download Client 2021-01-20 23:43:52 -08:00
Taloth Saldono
b66bf542c1 Typo for linux 2021-01-20 21:42:48 +01:00
Robin Dadswell
c05fccb90d Fixed: Error handling when cannot create folder in Recycling Bin
Closes #4163
2021-01-17 10:27:58 -08:00
Mark McDowall
ab478fd64b New: Treat Manual Bad in history as failed 2021-01-16 16:24:30 -08:00
bakerboy448
d016079f6b make HashedReleaseFixture entries generic 2021-01-16 15:48:47 -08:00
bakerboy448
4a7e5ac06e Fixed: Handle more obfuscated names
Closes #4198
2021-01-16 15:48:47 -08:00
Taloth Saldono
e10cff5414 Fixed parsing (duplicate) releases for series with multiple season number mappings 2021-01-16 21:13:32 +01:00
Taloth Saldono
6c17b4bb86 Fixed accounting for zero terminator in long path limitation
closes #4259
2021-01-16 02:49:48 +01:00
Mark McDowall
e704ee617f New: Require Encryption option for email 2021-01-15 17:29:43 -08:00
Mark McDowall
c2fcdb4457 Fixed: Managing display profiles on mobile 2021-01-15 17:29:43 -08:00
Mark McDowall
a6637b2911 Fixed: Sorting in Interactive search duplicates results
Closes #3964
2021-01-15 17:29:43 -08:00
Taloth Saldono
efb18223fe Fixed duplicate id searches due to missing Equals on SceneSeasonMapping 2021-01-16 01:32:08 +01:00
Taloth Saldono
c3e2f22adb Show separate message for unknown episode/series 2021-01-16 01:31:17 +01:00
Taloth Saldono
9de5181f01 Fixed: Regular Anime being caught in Chinese parser rules
closes #4257
2021-01-15 19:55:55 +01:00
Taloth Saldono
63607ad541 Fixed Agenda Time wrapping 2021-01-15 19:06:09 +01:00
Taloth Saldono
620359dcc6 Fixed: Updated BTN url to https
closes #4249
2021-01-14 23:30:47 +01:00
Taloth Saldono
881bbb654b Linting as usual 2021-01-14 23:20:09 +01:00
Taloth Saldono
c28cafba0a Fixed: Unnecessary certificate validation errors on localhost/loopback
closes #4215
2021-01-14 22:07:16 +01:00
Taloth Saldono
5668152d6f New: Added Scene Info to Interactive Search results to show more about the applied scene/TheXEM mappings 2021-01-14 22:07:16 +01:00
Taloth Saldono
dcda03da4a Fixed searching the wrong season. 2021-01-14 22:07:15 +01:00
Mark McDowall
ba2ca7ee29 New: Parsing of '[WEB]' as WebDL 2021-01-13 17:17:11 -08:00
bakerboy448
8077434a38 Update contributing.md 2021-01-13 17:00:09 -08:00
Qstick
a225b34806 Fix name of max NumberInput in QualityDefinition.js 2021-01-13 16:59:32 -08:00
bakerboy448
b181f8ae9f Readme updates 2021-01-13 16:59:08 -08:00
Qstick
579c443349 New: Replace SmtpClient with Mailkit
Closes #4213
2021-01-13 16:58:22 -08:00
bakerboy448
8a511a0e20 Fixed: Parse standalone UHD as 2160p if no other resolution info is present 2021-01-12 21:36:18 +01:00
Taloth Saldono
8bc0ab981c Fixed: Dailiezearch. 2021-01-11 21:04:16 +01:00
bakerboy448
741fa57f05 Update wiki link hints for health checks
Closes #4190
2021-01-09 12:39:30 -08:00
Mark McDowall
8a3027fa7c New: Allow quality size limits to be closer together 2021-01-06 22:01:50 -08:00
Mark McDowall
fddf2d1fc8 Better task interval fetching 2021-01-06 22:01:50 -08:00
Mark McDowall
6d44d6c1b7 Fixed: Only delete update folder if it exists 2021-01-06 22:01:50 -08:00
Taloth Saldono
7e045f3e3c Fixed tests 2021-01-04 22:18:08 +01:00
Taloth Saldono
f4d14301f1 No longer need the special tvdb season number handling since it's integrated into the search. 2021-01-04 21:41:05 +01:00
Taloth Saldono
17985f7a33 Fixed: Regression in searching anime by primary title 2021-01-04 20:55:13 +01:00
Taloth Saldono
772448b41b New: Support in services for multiple scene naming/numbering exceptions 2021-01-04 17:35:35 +01:00
Mark McDowall
ed2bb0d73a Fixed: Backups interval being used as minutes instead of days 2021-01-04 08:22:01 -08:00
Taloth Saldono
ae45089c62 Fixed tests 2021-01-03 21:44:46 +01:00
Taloth Saldono
f3695a41d7 Linting 2021-01-03 21:19:09 +01:00
bakerboy448
471f0016b4 Fixed: Additional handling for obfuscated releases
closes #4198
2021-01-03 21:19:03 +01:00
bakerboy448
056a699daf Fixed: Parsing of 4Kto1080p as 1080p
closes #4199
2021-01-03 21:18:48 +01:00
Mark McDowall
99be6a7e40 Use createHandleActions for adding/removing commands so itemMap is synced properly 2021-01-02 15:30:24 -08:00
Mark McDowall
c1d060ff58 New: Removing update folder from temp folder during housekeeping
Closes #4178
2021-01-02 15:13:01 -08:00
Mark McDowall
101b1ec743 New: Renamed Quick Import to Move Automatically
Close #4210
2021-01-02 12:14:16 -08:00
Mark McDowall
a4ffb256a6 Fixed UpdatePackageProviderFixture tests 2021-01-02 11:53:11 -08:00
Mark McDowall
ca52eb76ca Fixed: Don't convert series selection filter to lower case in state 2021-01-02 10:30:11 -08:00
Mark McDowall
37501094d7 Fixed: Restored robots.txt 2020-12-31 13:11:22 -08:00
Qstick
cfbb4a3235 Fixed: Timespan over 1 month shown incorrectly
Closes #4208
2020-12-31 11:51:22 -08:00
Mark McDowall
a2050803a2 Fixed: Missing leading 0 in minutes/seconds for media info duration
Closes #4208
2020-12-31 11:50:51 -08:00
Mark McDowall
e5e86680c8 Fixed: Backup interval is updated on change
Closes #4207
2020-12-31 11:42:00 -08:00
Mark McDowall
ca34f64eb0 Fixed: Update path before importing to ensure it hasn't changed
Closes #4206
2020-12-30 14:01:01 -08:00
tenshiak
c7b950f213 Fixed: Parsing Polish language 2020-12-30 12:11:47 -08:00
bakerboy448
36f231ea24 New: Rename Import to Library Import 2020-12-30 11:04:53 -08:00
Taloth Saldono
b60d4f46d2 eslint 2020-12-25 16:14:56 +01:00
Taloth Saldono
090cdc364e Small helper in UI to access Sonarr API more easily 2020-12-24 23:55:39 +01:00
Taloth Saldono
182ad17b77 Fixed: Series year wrong when airing January 1st.
closes #4191
2020-12-24 12:43:46 +01:00
Taloth Saldono
026af22229 Fixed: OSX version detection
ref #4113 (not a fix, just partially)
2020-12-24 12:43:31 +01:00
Qstick
078898af91 Fixed: Format Errors from AudioChannel formatter 2020-12-18 16:34:50 -08:00
Mark McDowall
8b8deb5646 Fixed Migration 148 test 2020-12-18 16:28:45 -08:00
Qstick
314a12ffb5 Fixed: Handle 3 digit audio channels 2020-12-18 16:28:33 -08:00
Nathaniel Peiffer
7b04e11c54 Fixed: Language parsing with space-delimited releases
Closes #4056
2020-12-17 16:55:07 -08:00
Qstick
9180e7d6fd Fixed: Don't workaround DTS if audioChannels invalid 2020-12-17 16:54:05 -08:00
Qstick
b8b09cd0ce Fixed: Migrate Mediainfo properties that changed names 2020-12-17 16:54:05 -08:00
Qstick
39cb0934bc Fixed: Use audioChannels_Original if it exists in MI 2020-12-17 16:54:05 -08:00
Mark McDowall
b84f84ad0a Fixed health check wiki link unit tests 2020-12-16 14:52:29 -08:00
Robin Dadswell
55a7253dc2 New: Sorting Series List/Mass Editor by Language Profile and Tags
Closes #3854
2020-12-14 20:07:00 -08:00
bakerboy448
e733529dc3 All Wiki links now use the consolidated Servarr wiki 2020-12-13 11:07:12 -08:00
Robin Dadswell
cc39d4ee23 Fixed: '/series' URL Base breaking UI navigation
Closes #4148
2020-12-13 10:57:06 -08:00
Robin Dadswell
0ff889c3be New: Added Series Monitoring Toggle to Series Details
Closes #3991
2020-12-13 09:58:56 -08:00
Mark McDowall
39589b8c02 Move config.yml for github 2020-12-06 11:37:03 -08:00
Mark McDowall
c5c0462258 Fixed: Using folder as scene name for season packs 2020-12-06 11:34:01 -08:00
bakerboy448
dafcba7336 Update GitHub templates
- remove other issue template
- update support and add wiki
- prevent blank issues
- add support links
2020-12-01 09:38:53 -08:00
bakerboy448
19ff7bdc30 Fixed: List Import no longer fails due to duplicates
Closes #4100
2020-11-25 16:51:22 -08:00
Taloth Saldono
a9384e26d8 Removed unnecessary importlists warning. 2020-11-23 11:04:23 +01:00
Taloth Saldono
0bc190e97e Fixed binary execute permissions for osx and Radarr 2020-11-22 17:10:29 +01:00
Taloth Saldono
2c76afb839 Fixed disk permission tests 2020-11-21 22:30:33 +01:00
Taloth Saldono
0edb7b77a1 Reverted temporary dev debug code change 2020-11-21 15:27:10 +01:00
Mark McDowall
59bffa66ad Fixed: Monitor 'None' won't monitor latest season 2020-11-20 14:43:54 -08:00
Mark McDowall
145c644c9d New: Validate that naming formats don't contain illegal characters 2020-11-20 14:43:54 -08:00
Taloth Saldono
d88bb7f855 New: Displaying folder-based permissions in UI rather than file-based permissions and with selectable sane presets
Fixed: Preserve setgid when applying unix permissions
2020-11-20 15:42:23 +01:00
Antoine Martin
850552bf17 Update indexer category parameters for the other nyaa 2020-11-17 22:20:56 -08:00
Mark McDowall
66a19424af Added tests for using folder name as scene name 2020-11-16 23:25:42 -08:00
Taloth Saldono
a234293146 Credit where credit is due 2020-11-16 21:31:13 +01:00
Taloth Saldono
5fced70948 Give systemd a bit more time to restart sonarr after update 2020-11-16 21:24:55 +01:00
ta264
7a0e1818c0 Fixed: Import single file torrents with a folder from QBittorrent
Signed-off-by: Taloth Saldono <Taloth@users.noreply.github.com>
2020-11-16 21:06:11 +01:00
Mark McDowall
bd0e5e16b8 Fixed unit tests 2020-11-15 23:49:02 -08:00
Mark McDowall
487c664e43 Fixed: Scene Name not being stored properly during import if not linked to a download client item and filename is obfuscated 2020-11-15 18:18:23 -08:00
Mark McDowall
fed2a429c7 New: Don't process files during Manual Import if there are more than 100 items
Closes #1142
2020-11-15 18:18:23 -08:00
Taloth Saldono
ad9e709d96 Fixed duplicate UpdateHistory items 2020-11-15 22:19:48 +01:00
Mark McDowall
4c58ea63d6 Fixed: Tags in tag editor in SafarIE
Closes #4071
2020-11-15 11:00:21 -08:00
Taloth Saldono
e5ec4e706a Readded Movies cat to the end of the Newznab list 2020-11-14 22:43:37 +01:00
Taloth Saldono
158e31d54a Fixed: Truncating too long filenames with unicode characters
closes #4085
2020-11-14 22:33:23 +01:00
Taloth Saldono
05820ac272 Protect against Qbittorrent edgecase if users add torrents manually with Keep top-level folder disabled 2020-11-14 22:33:22 +01:00
Mark McDowall
fe0d8bb7da Return max tooltip width 2020-11-13 17:16:05 -08:00
Mark McDowall
6f74a9e3eb Fixed: Reprocessing Manual Import items resetting season number if no episodes were selected
Closes #4089
2020-11-13 17:12:10 -08:00
Mark McDowall
d90f50d589 Fixed: Language chosen in manual import overridden during import
Closes #4082
2020-11-13 17:12:10 -08:00
Mark McDowall
517fc2bd75 Fixed: Example file names for Daily Series
Closes #4078
2020-11-13 17:12:10 -08:00
Mark McDowall
b6316c9fcd Spelling confidence 2020-11-13 17:12:10 -08:00
Mark McDowall
675d948a1f Fixed: Manual Import breaking if quality is selected before series 2020-11-13 17:12:10 -08:00
Taloth Saldono
49bf3f4512 And forgot test of course 2020-11-14 02:02:12 +01:00
Taloth Saldono
3ff848b019 Fix for QBittorrent directory for specific torrents
fixes #4091
2020-11-14 01:52:28 +01:00
Taloth Saldono
8b2550cef0 Bumped Sabnzbd default history request size from 30 to 60 2020-11-14 00:00:29 +01:00
ta264
813f886920 Fixed: QBittorrent imports when torrent name and folder name differ
closes #3346 
fixes #3968
closes #4068
2020-11-13 23:15:58 +01:00
Mark McDowall
c75c546888 Fixed: Manual import when quality was selected before episodes
Closes #4065
2020-11-08 21:33:43 -08:00
Mark McDowall
baa41b2c13 Fixed: Correctly storing v0 version during import, allowing them to be upgraded to v1 later
Closes #4066
2020-11-07 11:55:28 -08:00
Mark McDowall
aeaaa4a77a Fixed: wiki link for removed series health check
Closes #4053
2020-11-01 16:01:40 -08:00
Mark McDowall
1025bdc76e Show .net version in UI
Closes #4057
2020-11-01 16:01:40 -08:00
Mark McDowall
0b7aa19ac0 Fixed: Telegram silent notifications
Closes #4041
2020-11-01 16:01:40 -08:00
Robin Dadswell
cfdaddd81a New: Discord notification upgrade colour
Closes #4044
2020-11-01 16:01:31 -08:00
Timothy Pillow
fd4c2c11ad Change 'Ignore Deleted Episodes' to 'Unmonitor Deleted Episodes' for consistency 2020-11-01 16:00:02 -08:00
Robin Dadswell
42d93f6fdb New: Changed colour of Discord On Download notifications
Closes #4036
2020-10-26 07:45:50 -07:00
Mark McDowall
0de3f10701 Fixed: 1080i HDTV H264 incorrectly being detected as Raw HD 2020-10-25 17:17:57 -07:00
Mark McDowall
7b1876d0d8 Fixed: Cleanse account and passwd from Download Station URLs 2020-10-25 16:12:45 -07:00
Mark McDowall
91c395d0c6 Fixed: Show TLS errors in UI when testing download clients
Closes #4021
2020-10-25 15:59:40 -07:00
Mark McDowall
3fc3aef268 Improved Trakt list validation
New: Able to add empty Trakt lists (shows warning during test)
Fixed: Show error in UI if Trakt list hasn't been authenticated

Closes #4022
2020-10-25 12:01:29 -07:00
Mark McDowall
fa2e70d571 Fixed: Show feed URL if incorrect mime type is found 2020-10-25 12:01:29 -07:00
Mark McDowall
c823654041 Fixed: Discord notifications failing if episode overview is missing 2020-10-25 12:01:29 -07:00
Mark McDowall
cfa0c6d0d7 Fixed phrasing when release match found by ID 2020-10-25 12:01:29 -07:00
Taloth Saldono
4d4319797b Updated debian install script to handle longer user names 2020-10-24 10:41:58 +02:00
Mark McDowall
e90e144ebc Fixed: Grab/Import fields for Discord notifications being duplicated 2020-10-18 19:24:21 -07:00
Qstick
f701adaef5 Pass no parameter instead of null parameter on Kodi Update 2020-10-18 17:50:07 -07:00
Mark McDowall
427ce17e6e Kodi GetMovies fails due to Parameter Type 2020-10-18 17:50:02 -07:00
Qstick
3a8522e92f Resource missing from Gotify call 2020-10-18 17:44:09 -07:00
Qstick
886e5581d8 Gotify token as query parameter 2020-10-18 17:43:57 -07:00
Qstick
470c9101ae New: Customizable Discord Notifications
Closes #3985
2020-10-18 17:09:26 -07:00
Qstick
09347f79c5 TagSelect field type 2020-10-18 16:27:00 -07:00
ta264
1d02208316 New: Make Twitter NetStandard compatible 2020-10-18 16:26:59 -07:00
Mark McDowall
0878f514aa New: Remove Growl notifications 2020-10-18 16:26:58 -07:00
Mark McDowall
1b32949443 Rename migration 144 2020-10-18 15:32:44 -07:00
Qstick
665b80f15c Convert Notifications from RestSharp to HttpClient 2020-10-18 15:22:24 -07:00
Mark McDowall
51528f63e9 Fixed: Parsing of some daily episodes 2020-10-18 15:07:26 -07:00
Mark McDowall
a7e9eebfed Fix namespace for BlacklistBulkResource 2020-10-18 15:07:26 -07:00
Qstick
897673b459 Improve use of All() and speedup Unmapped Folder matching
Co-authored-by: ta264 <ta264@users.noreply.github.com>
2020-10-18 14:59:26 -07:00
Mark McDowall
19f724dcd9 Fixed: Limit Raw HD detection by MPEG-2 to HDTV sources 2020-10-13 00:06:12 -07:00
Mark McDowall
4ad137f1eb Fixed: Size on disk sorting and display
Closes #4014
2020-10-12 16:27:21 -07:00
Mark McDowall
dab6242ff2 New: Ability to edit restriction terms in Release Profiles 2020-10-12 15:30:32 -07:00
Mark McDowall
d47ad37791 Fix installer branch/build from testing 2020-10-12 12:19:34 -07:00
Mark McDowall
2adedb97da New: Differentiate between short term and long term (more than 6 hours) indexer failures
Closes #3279
2020-10-12 11:24:19 -07:00
Mark McDowall
7dd64d843a Fixed: (Windows) clean up extraneous files in build folder during installation 2020-10-12 11:03:47 -07:00
Mark McDowall
f30ae69c10 New: Reprocess items after selection in Manual Import
Closes #3818
2020-10-12 10:49:35 -07:00
Mark McDowall
c871b3f948 Fixed: Copying passwords
Closes #4011
2020-10-11 16:42:07 -07:00
Mark McDowall
67f5628340 New: Bulk remove from Blacklist
Closes #3500
2020-10-11 15:28:41 -07:00
Mark McDowall
fae38a107f New: Warning when combining preferred words with a specific indexer 2020-10-11 15:28:41 -07:00
Mark McDowall
f35b8174aa Re-saving edited providers will forcibly save them 2020-10-11 15:28:40 -07:00
Taloth Saldono
465de11c90 Fixed: Regression causing updater to fail (manual update required if on 3.0.3.971, see forums) 2020-10-11 20:52:12 +02:00
735 changed files with 21116 additions and 10689 deletions

View File

@@ -1,41 +0,0 @@
<!--
Before opening a new issue, please ensure:
- You use the forums for support/questions
- You search for existing bugs/feature requests
- Remove extraneous template details
- Do not prefix title with type of issue (Feature Request, Bug, etc.) The appropriate labels will be added during triage.
-->
## Support / Questions
Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.
<!--
Remove if not opening a bug report
-->
## Bug Report
### System Information/Logs
**Sonarr Version:**
**Operating System:**
**.net Framework (Windows) or mono (macOS/Linux) Version:**
**Link to Log Files (debug or trace):**
**Browser (for UI bugs):**
### Additional Information
<!--
Remove if not opening a feature request
-->
## Feature Request
### What problem are you looking to solve?
### Other Information

View File

@@ -1,28 +0,0 @@
---
name: Bug report
about: Create a report to help us improve Sonarr
---
**Describe the bug**
A clear and concise description of what the bug is.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Logs**
Link to debug or trace log files.
**System Information**
- Sonarr Version: [e.g. 2.0.0.1]
- Operating System: [e.g. Windows 10]
- .net Framework (Windows) or mono (macOS/Linux) Version: [e.g. 4.5 or 5.12]
**UI Bugs:**
- OS: [e.g. Windows]
- Browser: [e.g. chrome, firefox]
- Version: [e.g. 22]
**Additional context**
Add any other context about the problem here.

72
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,72 @@
name: Bug Report
description: 'Support Requests will be closed immediately, if you are not 100% certain this is a bug please go to our Reddit, Discord, Forums, or IRC first. Exceptions do not mean you found a bug!'
labels: ['needs-triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Sonarr**: Sonarr 3.0.6.1265
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
value: |
- OS:
- Sonarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Main
- Develop
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Trace Logs (https://wiki.servarr.com/sonarr/troubleshooting#logging-and-log-files)
Links? References? Anything that will give us more context about the issue you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true

14
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Support via Discord
url: https://discord.gg/M6BvZn5
about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/Sonarr
about: Discuss and search through support topics.
- name: Support via Forums
url: https://forums.sonarr.tv/
about: Discuss and search through support topics.
- name: Support via IRC
url: https://web.libera.chat/?channels=#sonarr
about: Chat with users and devs on support and setup related topics.

View File

@@ -1,14 +0,0 @@
---
name: Feature request
about: Suggest an idea for Sonarr
---
**Describe the problem**
A clear and concise description of the problem you're looking to solve.
**Describe any solutions you think might work**
A clear and concise description of any solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,38 @@
name: Feature Request
description: 'Suggest an idea for Sonarr'
labels: ['needs-triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the feature you are requesting.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe
description: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: true
- type: textarea
attributes:
label: Anything else?
description: |
Links? References? Mockups? Anything that will give us more context about the feature you are encountering!
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
validations:
required: true

View File

@@ -1,7 +0,0 @@
---
name: Other issues
about: How to get support or ask questions
---
Please use https://forums.sonarr.tv/ for support. Support requests or questions will be redirected to the forums and the issue will be closed.

View File

@@ -6,7 +6,7 @@ A few sentences describing the overall goals of the pull request's commits.
#### Todos
- [ ] Tests
- [ ] Documentation
- [ ] Wiki Updates
#### Issues Fixed or Closed by this PR

6
.github/SUPPORT.md vendored
View File

@@ -1,7 +1,7 @@
## Support
There are a number of frequently asked questions that have been answered in our [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
There are a number of frequently asked questions that have been answered in our [FAQ](https://wiki.servarr.com/sonarr/faq)
The [wiki](https://github.com/Sonarr/Sonarr/wiki) contains other information and guides
The [wiki](https://wiki.servarr.com/sonarr) contains other information and guides
If you have a support question, please use the [support forums](https://forums.sonarr.tv/).
Please use one of the support channels: [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord ](https://discord.gg/M6BvZn5), or [IRC](https://web.libera.chat/?channels=#sonarr)for support/questions.

21
.github/workflows/lock.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: 'Lock threads'
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '90'
issue-exclude-created-before: ''
issue-exclude-labels: 'one-day-maybe'
issue-lock-labels: ''
issue-lock-comment: ''
issue-lock-reason: 'resolved'
process-only: ''

1
.gitignore vendored
View File

@@ -141,3 +141,4 @@ output/*
_start
src/.idea/
/distribution/windows/setup/output/*

View File

@@ -3,25 +3,40 @@
We're always looking for people to help make Sonarr even better, there are a number of ways to contribute.
## Documentation ##
Setup guides, FAQ, the more information we have on the wiki the better.
Setup guides, [FAQ](https://wiki.servarr.com/Sonarr_FAQ), the more information we have on the [wiki](https://wiki.servarr.com/Sonarr) the better.
## Development ##
See the readme for information on setting up your development environment.
### Tools required ###
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download/) (Node 10.X.X or higher)
- [Yarn](https://yarnpkg.com/)
### Getting started ###
1. Fork Sonarr
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
3. Install the required Node Packages `yarn install`
4. Start webpack to monitor your dev environment for any frontend changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Sonarr.Console` and framework to `x86`
6. Debug the project in Visual Studio
7. Open http://localhost:8989
### 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)
- Rebase from Sonarr's develop branch, don't merge
- Rebase from Sonarr's `develop` branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the forums or on IRC if you have any questions
- Reach out to us on our [forums](https://forums.sonarr.tv/), [subreddit](https://www.reddit.com/r/sonarr/), [discord](https://discord.gg/Ex7FmFK), or [IRC](http://webchat.freenode.net/?channels=#sonarr) if you have any questions
- Add tests (unit/integration)
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2012 and WebStorm (to my knowledge)
- Use 4 spaces instead of tabs, this should be the default for VS 2019 and WebStorm
### Pull Requesting ###
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
- Only make pull requests to develop (currently phantom-develop), never master, if you make a PR to master we'll comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)

View File

@@ -4,17 +4,23 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
## Getting Started
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
- [Download/Installation](https://sonarr.tv/#downloads-v3)
- [FAQ](https://wiki.servarr.com/sonarr/faq)
- [Wiki](https://wiki.servarr.com/Sonarr)
- [(WIP) API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
- [Donate](https://sonarr.tv/donate)
## Support
Note: GitHub Issues are for Bugs and Feature Requests Only
- [Donate](https://sonarr.tv/donate)
- [Forums](https://forums.sonarr.tv/)
- [Discord](https://discord.gg/M6BvZn5)
- [GitHub - Bugs and Feature Requests Only](https://github.com/Sonarr/Sonarr/issues)
- [IRC](https://web.libera.chat/?channels=#sonarr)
- [Reddit](https://www.reddit.com/r/sonarr)
- [Wiki](https://wiki.servarr.com/sonarr)
## Features
@@ -32,43 +38,16 @@ Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS fee
- Full support for specials and multi-episode releases
- And a beautiful UI
## Configuring Development Environment:
## Contributing
### Requirements
- [Visual Studio 2017](https://www.visualstudio.com/vs)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download)
- [Yarn](https://yarnpkg.com)
### Setup
- Make sure all the required software mentioned above are installed
- Clone the repository recursively to get Sonarr and it's submodules
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
- Install the required Node Packages using `yarn`
### Backend Development
- Run `yarn build` to build the UI
- Open `Sonarr.sln` in Visual Studio
- Make sure `Sonarr.Console` is set as the startup project
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
- Build Solution
### UI Development
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
- Run Sonarr.Console.exe (or debug in Visual Studio)
### Licenses
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2020
### Development
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Sonarr/Sonarr/graphs/contributors"><img src="https://opencollective.com/Sonarr/contributors.svg?width=890&button=false" /></a>
### Supporters
This project would not be possible without the support of our users and software providers. [**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
This project would not be possible without the support of our users and software providers.
[**Become a sponsor or backer**](https://opencollective.com/sonarr) to help us out!
#### Sponsors
@@ -90,3 +69,7 @@ Thank you to [<img src="/Logo/Jetbrains/jetbrains.svg" alt="JetBrains" width="32
* [<img src="/Logo/Jetbrains/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/Jetbrains/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### Licenses
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
- Copyright 2010-2021

View File

@@ -141,7 +141,7 @@ Build()
ProgressEnd 'Build'
}
RunGulp()
RunWebpack()
{
ProgressStart 'yarn install'
yarn install
@@ -149,9 +149,9 @@ RunGulp()
LintUI
ProgressStart 'Running gulp'
CheckExitCode yarn run build --production
ProgressEnd 'Running gulp'
ProgressStart 'Running webpack'
CheckExitCode yarn run build --env production
ProgressEnd 'Running webpack'
}
CreateMdbs()
@@ -447,7 +447,7 @@ esac
UpdateVersionNumber
Build
CreateReleaseInfo
RunGulp
RunWebpack
PackageMono
PackageMacOS
PackageMacOSApp

View File

@@ -1,4 +1,5 @@
fromdos ./debian/*
chmod ugo-x ./debian/*
cp -r ./debian ./debian_backup
BuildVersion=${dependent_build_number:-3.10.0.999}

View File

@@ -1 +1 @@
8
10

View File

@@ -8,7 +8,10 @@ db_input high sonarr/owning_group || true
db_endblock
db_go
db_beginblock
db_input low sonarr/owning_umask || true
db_input low sonarr/config_directory || true
db_endblock
db_go
exit 0

View File

@@ -9,6 +9,8 @@ db_get sonarr/owning_user
USER="$RET"
db_get sonarr/owning_group
GROUP="$RET"
db_get sonarr/owning_umask
UMASK="$RET"
db_get sonarr/config_directory
CONFDIR="$RET"
@@ -64,9 +66,11 @@ fi
# Create data directory
if [ ! -d "$CONFDIR" ]; then
mkdir -p "$CONFDIR"
chown -R $USER:$GROUP "$CONFDIR"
fi
# Set permissions on data directory (always do this instead only on creation in case user was changed via dpkg-reconfigure)
chown -R $USER:$GROUP "$CONFDIR"
#BEGIN BUILTIN UPDATER
# Apply patch if present
if [ "$UPDATER" = "BuiltIn" ] && [ -f /usr/lib/sonarr/bin_patch/release_info ]; then
@@ -92,7 +96,7 @@ fi
chown -R $USER:$GROUP /usr/lib/sonarr
# Update sonarr.service file
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
sed -i "s:User=\w*:User=$USER:g; s:Group=\w*:Group=$GROUP:g; s:UMask=[0-9]*:UMask=$UMASK:g; s:-data=.*$:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
#BEGIN BUILTIN UPDATER
if [ "$UPDATER" = "BuiltIn" ]; then

View File

@@ -9,7 +9,7 @@ UPDATER={updater}
# Existing nzbdrone packages do not have startup scripts and the process might still be running.
# If the user manually installed nzbdrone then the process might still be running too.
if [ $1 = "install" ]; then
psNzbDrone=`ps ax -o'user,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
psNzbDrone=`ps ax -o'user:20,pid,ppid,unit,args' | grep mono.*NzbDrone\\\\.exe || true`
if [ ! -z "$psNzbDrone" ]; then
# Get the user and optional systemd unit
psNzbDroneUser=`echo "$psNzbDrone" | tr -s ' ' | cut -d ' ' -f 1`

View File

@@ -1,2 +1,3 @@
ignores msbuild
ignores libmediainfo0v5
ignores libc6

View File

@@ -1,3 +1,6 @@
# This file is owned by the sonarr package, DO NOT MODIFY MANUALLY
# Instead use 'dpkg-reconfigure -plow sonarr' to modify User/Group/UMask/-data
# Or use systemd built-in override functionality using 'systemctl edit sonarr'
[Unit]
Description=Sonarr Daemon
After=network.target

View File

@@ -14,6 +14,12 @@ Description: Sonarr group:
Any media files created by Sonarr will be writeable by this group.
It's advisable to keep the group the same between download client, Sonarr and media centers.
Template: sonarr/owning_umask
Type: string
Default: 0002
Description: Sonarr umask:
Specifies the umask of the files created by Sonarr. 0002 means the files will be created with 664 as permissions.
Template: sonarr/config_directory
Type: string
Default: /var/lib/sonarr

View File

@@ -1,16 +1,25 @@
FROM ubuntu:xenial AS builder
FROM ubuntu:focal AS builder
ENV DEBIAN_FRONTEND noninteractive
ENV MONO_VERSION 5.18
RUN apt-get update && \
apt-get -y -o Dpkg::Options::="--force-confold" install --no-install-recommends \
apt-transport-https \
wget dirmngr gpg gpg-agent \
# add-apt-repository for PPAs
software-properties-common && \
rm -rf /var/lib/apt/lists/*
RUN apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF && \
echo "deb http://download.mono-project.com/repo/debian stable-xenial/snapshots/$MONO_VERSION main" > /etc/apt/sources.list.d/mono-official-stable.list && \
echo "deb http://download.mono-project.com/repo/debian stable-focal main" > /etc/apt/sources.list.d/mono-official-stable.list && \
apt-get update && apt-get install -y \
devscripts build-essential tofrodos \
dh-make dh-systemd \
cli-common-dev \
mono-complete \
sqlite3 libcurl3 mediainfo
sqlite3 libcurl4 mediainfo
RUN apt-get upgrade -y
RUN apt-cache policy mono-complete
RUN apt-cache policy cli-common-dev

View File

@@ -37,6 +37,7 @@ Compression=lzma2/normal
AppContact={#ForumsURL}
VersionInfoVersion={#BuildNumber}
SetupLogging=yes
OutputDir=output
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -59,11 +60,12 @@ Name: "{userstartup}\{#AppName}"; Filename: "{app}\Sonarr.exe"; WorkingDir: "{ap
[InstallDelete]
Name: "{commonappdata}\NzbDrone\bin"; Type: filesandordirs
Name: "{app}"; Type: filesandordirs
[Run]
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated;
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u /exitimmediately"; Flags: runhidden waituntilterminated;
Filename: "{app}\Sonarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl /exitimmediately"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
Filename: "{app}\Sonarr.Console.exe"; StatusMsg: "Installing Windows Service"; Parameters: "/i /exitimmediately"; Flags: runhidden waituntilterminated; Tasks: windowsService
Filename: "{app}\Sonarr.exe"; Description: "Open Sonarr Web UI"; Flags: postinstall skipifsilent nowait; Tasks: windowsService;
Filename: "{app}\Sonarr.exe"; Description: "Start Sonarr"; Flags: postinstall skipifsilent nowait; Tasks: startupShortcut none;

View File

@@ -17,6 +17,9 @@ RUN fromdos /startup.sh
WORKDIR /data/
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
RUN groupadd sonarrtst -g 4020 && useradd sonarrtst -u 4021 -g 4020 -m -s /bin/bash
USER sonarrtst
CMD bash /startup.sh

View File

@@ -25,5 +25,8 @@ RUN fromdos /startup.sh
WORKDIR /data/
VOLUME ["/data/_tests_linux", "/data/_output_linux", "/data/_tests_results"]
RUN groupadd sonarrtst -g 4020 && useradd sonarrtst -u 4021 -g 4020 -m -s /bin/bash
USER sonarrtst
CMD bash /startup.sh

View File

@@ -75,7 +75,7 @@
"function-parentheses-newline-inside": "never-multi-line",
"function-parentheses-space-inside": "never",
"function-url-quotes": "always",
"function-url-scheme-blacklist": [
"function-url-scheme-disallowed-list": [
"data"
],
"function-whitespace-after": "always",

View File

@@ -0,0 +1,262 @@
const path = require('path');
const webpack = require('webpack');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const TerserPlugin = require('terser-webpack-plugin');
module.exports = (env) => {
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = !!env.production;
const isProfiling = isProduction && !!env.profile;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
}
},
output: {
path: distFolder,
publicPath: '/',
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
moduleIds: 'deterministic',
chunkIds: 'named',
splitChunks: {
chunks: 'initial',
name: 'vendors'
}
},
performance: {
hints: false
},
plugins: [
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: 'Content/styles.css'
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
}),
new FileManagerPlugin({
events: {
onEnd: {
copy: [
// HTML
{
source: 'frontend/src/*.html',
destination: distFolder
},
// Fonts
{
source: 'frontend/src/Content/Fonts/*.*',
destination: path.join(distFolder, 'Content/Fonts')
},
// Icon Images
{
source: 'frontend/src/Content/Images/Icons/*.*',
destination: path.join(distFolder, 'Content/Images/Icons')
},
// Images
{
source: 'frontend/src/Content/Images/*.*',
destination: path.join(distFolder, 'Content/Images')
},
// Robots
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
}
]
}
}
}),
new LiveReloadPlugin()
],
resolveLoader: {
modules: [
'node_modules',
'frontend/build/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
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',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
config: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
return config;
};

View File

@@ -1,17 +0,0 @@
const gulp = require('gulp');
require('./clean');
require('./copy');
require('./webpack');
gulp.task('build',
gulp.series('clean',
gulp.parallel(
'webpack',
'copyHtml',
'copyFonts',
'copyImages'
)
)
);

View File

@@ -1,8 +0,0 @@
const gulp = require('gulp');
const del = require('del');
const paths = require('./helpers/paths');
gulp.task('clean', () => {
return del([paths.dest.root]);
});

View File

@@ -1,34 +0,0 @@
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('copyHtml', () => {
return gulp.src(paths.src.html, { base: paths.src.root })
.pipe(cache('copyHtml'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyFonts', () => {
return gulp.src(
path.join(paths.src.fonts, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyFonts'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});
gulp.task('copyImages', () => {
return gulp.src(
path.join(paths.src.images, '**', '*.*'), { base: paths.src.root }
)
.pipe(cache('copyImages'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});

View File

@@ -1,5 +0,0 @@
require('./build.js');
require('./clean.js');
require('./copy.js');
require('./watch.js');
require('./webpack.js');

View File

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

View File

@@ -1,23 +0,0 @@
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/`,
exclude: {
libs: `!${root}/JsLibraries/**`
}
},
dest: {
root: './_output/UI/',
content: './_output/UI/Content/',
fonts: './_output/UI/Content/Fonts/',
images: './_output/UI/Content/Images/'
}
};
module.exports = paths;

View File

@@ -1,18 +0,0 @@
const gulp = require('gulp');
const livereload = require('gulp-livereload');
const gulpWatch = require('gulp-watch');
const paths = require('./helpers/paths.js');
require('./copy.js');
require('./webpack.js');
function watch() {
livereload.listen({ start: true });
gulp.task('webpackWatch')();
gulpWatch(paths.src.html, gulp.series('copyHtml'));
gulpWatch(`${paths.src.fonts}**/*.*`, gulp.series('copyFonts'));
gulpWatch(`${paths.src.images}**/*.*`, gulp.series('copyImages'));
}
gulp.task('watch', gulp.series('build', watch));

View File

@@ -1,268 +0,0 @@
const gulp = require('gulp');
const webpackStream = require('webpack-stream');
const livereload = require('gulp-livereload');
const path = require('path');
const webpack = require('webpack');
const errorHandler = require('./helpers/errorHandler');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
const TerserPlugin = require('terser-webpack-plugin');
const uiFolder = 'UI';
const frontendFolder = path.join(__dirname, '..');
const srcFolder = path.join(frontendFolder, 'src');
const isProduction = process.argv.indexOf('--production') > -1;
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
const inlineWebWorkers = 'no-fallback';
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
console.log('Source Folder:', srcFolder);
console.log('Output Folder:', distFolder);
console.log('isProduction:', isProduction);
console.log('isProfiling:', isProfiling);
const cssVarsFiles = [
'../src/Styles/Variables/colors',
'../src/Styles/Variables/dimensions',
'../src/Styles/Variables/fonts',
'../src/Styles/Variables/animations',
'../src/Styles/Variables/zIndexes'
].map(require.resolve);
// Override the way HtmlWebpackPlugin injects the scripts
// TODO: Find a better way to get these paths without
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
const head = assetTags.headTags.map((v) => {
const href = v.attributes.href
.replace('\\', '/')
.replace('%5C', '/');
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
});
const body = assetTags.bodyTags.map((v) => {
v.attributes = { src: `/${v.attributes.src}` };
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
});
return html
.replace('<!-- webpack bundles head -->', head.join('\r\n '))
.replace('<!-- webpack bundles body -->', body.join('\r\n '));
};
const plugins = [
new webpack.DefinePlugin({
__DEV__: !isProduction,
'process.env.NODE_ENV': isProduction ? JSON.stringify('production') : JSON.stringify('development')
}),
new MiniCssExtractPlugin({
filename: path.join('Content', 'styles.css')
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.html',
filename: 'index.html'
})
];
const config = {
mode: isProduction ? 'production' : 'development',
devtool: '#source-map',
stats: {
children: false
},
watchOptions: {
ignored: /node_modules/
},
entry: {
index: 'index.js'
},
resolve: {
modules: [
srcFolder,
path.join(srcFolder, 'Shims'),
'node_modules'
],
alias: {
jquery: 'jquery/src/jquery'
}
},
output: {
path: distFolder,
filename: '[name].js',
sourceMapFilename: '[file].map'
},
optimization: {
chunkIds: 'named',
splitChunks: {
chunks: 'initial'
}
},
performance: {
hints: false
},
plugins,
resolveLoader: {
modules: [
'node_modules',
'frontend/gulp/webpack/'
]
},
module: {
rules: [
{
test: /\.worker\.js$/,
use: {
loader: 'worker-loader',
options: {
filename: '[name].js',
inline: inlineWebWorkers
}
}
},
{
test: /\.js?$/,
exclude: /(node_modules|JsLibraries)/,
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',
corejs: 3
}
]
]
}
}
]
},
// CSS Modules
{
test: /\.css$/,
exclude: /(node_modules|globals.css)/,
use: [
{ loader: MiniCssExtractPlugin.loader },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: {
localIdentName: '[name]/[local]/[hash:base64:5]'
}
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
config: {
ctx: {
cssVarsFiles
},
path: 'frontend/postcss.config.js'
}
}
}
]
},
// Global styles
{
test: /\.css$/,
include: /(node_modules|globals.css)/,
use: [
'style-loader',
{
loader: 'css-loader'
}
]
},
// Fonts
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
},
{
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}
}
]
}
]
}
};
if (isProfiling) {
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
config.optimization.minimizer = [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true, // Must be set to true if using source-maps in production
terserOptions: {
mangle: false,
keep_classnames: true,
keep_fnames: true
}
})
];
}
gulp.task('webpack', () => {
return webpackStream(config)
.pipe(gulp.dest('_output/UI'));
});
gulp.task('webpackWatch', () => {
config.watch = true;
return webpackStream(config)
.on('error', errorHandler)
.pipe(gulp.dest('_output/UI'))
.on('error', errorHandler)
.pipe(livereload())
.on('error', errorHandler);
});

View File

@@ -1,23 +1,32 @@
const reload = require('require-nocache')(module);
module.exports = (ctx, configPath, options) => {
const config = {
plugins: {
'postcss-mixins': {
mixinsDir: [
'frontend/src/Styles/Mixins'
]
},
'postcss-simple-vars': {
variables: () =>
ctx.options.cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
},
'postcss-color-function': {},
'postcss-nested': {}
}
};
const cssVarsFiles = [
'./src/Styles/Variables/colors',
'./src/Styles/Variables/dimensions',
'./src/Styles/Variables/fonts',
'./src/Styles/Variables/animations',
'./src/Styles/Variables/zIndexes'
].map(require.resolve);
return config;
const mixinsFiles = [
'frontend/src/Styles/Mixins/cover.css',
'frontend/src/Styles/Mixins/linkOverlay.css',
'frontend/src/Styles/Mixins/scroller.css',
'frontend/src/Styles/Mixins/truncate.css'
];
module.exports = {
plugins: [
['postcss-mixins', {
mixinsFiles
}],
['postcss-simple-vars', {
variables: () =>
cssVarsFiles.reduce((acc, vars) => {
return Object.assign(acc, reload(vars));
}, {})
}],
'postcss-color-function',
'postcss-nested'
]
};

View File

@@ -1,123 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
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 PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import BlacklistRowConnector from './BlacklistRowConnector';
class Blacklist extends Component {
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
return (
<PageContent title="Blacklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Clear"
iconName={icons.CLEAR}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load blacklist</div>
}
{
isPopulated && !error && !items.length &&
<div>
No history blacklist
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
columns={columns}
{...otherProps}
>
<TableBody>
{
items.map((item) => {
return (
<BlacklistRowConnector
key={item.id}
columns={columns}
{...item}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
</PageContent>
);
}
}
Blacklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onClearBlacklistPress: PropTypes.func.isRequired
};
export default Blacklist;

View File

@@ -1,154 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import Blacklist from './Blacklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, isClearingBlacklistExecuting) => {
return {
isClearingBlacklistExecuting,
...blacklist
};
}
);
}
const mapDispatchToProps = {
...blacklistActions,
executeCommand
};
class BlacklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlacklist,
gotoBlacklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlacklist();
} else {
gotoBlacklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlacklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlacklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlacklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlacklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlacklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlacklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlacklistPage({ page });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
//
// Render
render() {
return (
<Blacklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
{...this.props}
/>
);
}
}
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,
gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
setBlacklistSort: PropTypes.func.isRequired,
setBlacklistTableOption: PropTypes.func.isRequired,
clearBlacklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
);

View File

@@ -0,0 +1,232 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import { align, icons, kinds } from 'Helpers/Props';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
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 PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import BlocklistRowConnector from './BlocklistRowConnector';
class Blocklist extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isConfirmRemoveModalOpen: false,
items: props.items
};
}
componentDidUpdate(prevProps) {
const {
items
} = this.props;
if (hasDifferentItems(prevProps.items, items)) {
this.setState((state) => {
return {
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
items
};
});
return;
}
}
//
// Control
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
items,
columns,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
...otherProps
} = this.props;
const {
allSelected,
allUnselected,
selectedState,
isConfirmRemoveModalOpen
} = this.state;
const selectedIds = this.getSelectedIds();
return (
<PageContent title="Blocklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
onPress={this.onRemoveSelectedPress}
/>
<PageToolbarButton
label="Clear"
iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
/>
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
{...otherProps}
columns={columns}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>Unable to load blocklist</div>
}
{
isPopulated && !error && !items.length &&
<div>
No history blocklist
</div>
}
{
isPopulated && !error && !!items.length &&
<div>
<Table
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
columns={columns}
{...otherProps}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<BlocklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
<TablePager
totalRecords={totalRecords}
isFetching={isFetching}
{...otherProps}
/>
</div>
}
</PageContentBody>
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blocklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
</PageContent>
);
}
}
Blocklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
};
export default Blocklist;

View File

@@ -0,0 +1,160 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import withCurrentPage from 'Components/withCurrentPage';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import Blocklist from './Blocklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, isClearingBlocklistExecuting) => {
return {
isClearingBlocklistExecuting,
...blocklist
};
}
);
}
const mapDispatchToProps = {
...blocklistActions,
executeCommand
};
class BlocklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlocklist,
gotoBlocklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlocklist();
} else {
gotoBlocklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlocklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlocklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlocklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlocklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
}
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
}
}
//
// Render
render() {
return (
<Blocklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
{...this.props}
/>
);
}
}
BlocklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlocklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
);

View File

@@ -9,7 +9,7 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
class BlacklistDetailsModal extends Component {
class BlocklistDetailsModal extends Component {
//
// Render
@@ -77,7 +77,7 @@ class BlacklistDetailsModal extends Component {
}
}
BlacklistDetailsModal.propTypes = {
BlocklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
@@ -86,4 +86,4 @@ BlacklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired
};
export default BlacklistDetailsModal;
export default BlocklistDetailsModal;

View File

@@ -2,16 +2,17 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import SeriesTitleLink from 'Series/SeriesTitleLink';
import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlacklistRow.css';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
class BlacklistRow extends Component {
class BlocklistRow extends Component {
//
// Lifecycle
@@ -40,6 +41,7 @@ class BlacklistRow extends Component {
render() {
const {
id,
series,
sourceTitle,
language,
@@ -48,12 +50,20 @@ class BlacklistRow extends Component {
protocol,
indexer,
message,
isSelected,
columns,
onSelectedChange,
onRemovePress
} = this.props;
return (
<TableRow>
<TableSelectCell
id={id}
isSelected={isSelected}
onSelectedChange={onSelectedChange}
/>
{
columns.map((column) => {
const {
@@ -142,7 +152,7 @@ class BlacklistRow extends Component {
/>
<IconButton
title="Remove from blacklist"
title="Remove from blocklist"
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
@@ -155,7 +165,7 @@ class BlacklistRow extends Component {
})
}
<BlacklistDetailsModal
<BlocklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
@@ -169,7 +179,7 @@ class BlacklistRow extends Component {
}
BlacklistRow.propTypes = {
BlocklistRow.propTypes = {
id: PropTypes.number.isRequired,
series: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
@@ -179,8 +189,10 @@ BlacklistRow.propTypes = {
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
message: PropTypes.string,
isSelected: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onRemovePress: PropTypes.func.isRequired
};
export default BlacklistRow;
export default BlocklistRow;

View File

@@ -1,8 +1,8 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
import BlacklistRow from './BlacklistRow';
import BlocklistRow from './BlocklistRow';
function createMapStateToProps() {
return createSelector(
@@ -18,9 +18,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
onRemovePress() {
dispatch(removeFromBlacklist({ id: props.id }));
dispatch(removeBlocklistItem({ id: props.id }));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);

View File

@@ -1,7 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import Link from 'Components/Link/Link';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@@ -22,6 +23,7 @@ function HistoryDetails(props) {
const {
indexer,
releaseGroup,
preferredWordScore,
nzbInfoUrl,
downloadClient,
downloadId,
@@ -40,24 +42,35 @@ function HistoryDetails(props) {
/>
{
!!indexer &&
indexer ?
<DescriptionListItem
title="Indexer"
data={indexer}
/>
/> :
null
}
{
!!releaseGroup &&
releaseGroup ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Release Group"
data={releaseGroup}
/>
/> :
null
}
{
!!nzbInfoUrl &&
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
{
nzbInfoUrl ?
<span>
<DescriptionListItemTitle>
Info URL
@@ -66,39 +79,44 @@ function HistoryDetails(props) {
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span>
</span> :
null
}
{
!!downloadClient &&
downloadClient ?
<DescriptionListItem
title="Download Client"
data={downloadClient}
/>
/> :
null
}
{
!!downloadId &&
downloadId ?
<DescriptionListItem
title="Grab ID"
data={downloadId}
/>
/> :
null
}
{
!!indexer &&
age || ageHours || ageMinutes ?
<DescriptionListItem
title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)}
/>
/> :
null
}
{
!!publishedDate &&
publishedDate ?
<DescriptionListItem
title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/>
/> :
null
}
</DescriptionList>
);
@@ -118,11 +136,12 @@ function HistoryDetails(props) {
/>
{
!!message &&
message ?
<DescriptionListItem
title="Message"
data={message}
/>
/> :
null
}
</DescriptionList>
);
@@ -130,6 +149,7 @@ function HistoryDetails(props) {
if (eventType === 'downloadFolderImported') {
const {
preferredWordScore,
droppedPath,
importedPath
} = data;
@@ -143,21 +163,32 @@ function HistoryDetails(props) {
/>
{
!!droppedPath &&
droppedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Source"
data={droppedPath}
/>
/> :
null
}
{
!!importedPath &&
importedPath ?
<DescriptionListItem
descriptionClassName={styles.description}
title="Imported To"
data={importedPath}
/>
/> :
null
}
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
</DescriptionList>
);
@@ -165,7 +196,8 @@ function HistoryDetails(props) {
if (eventType === 'episodeFileDeleted') {
const {
reason
reason,
preferredWordScore
} = data;
let reasonMessage = '';
@@ -195,6 +227,15 @@ function HistoryDetails(props) {
title="Reason"
data={reasonMessage}
/>
{
preferredWordScore && preferredWordScore !== '0' ?
<DescriptionListItem
title="Preferred Word Score"
data={formatPreferredWordScore(preferredWordScore)}
/> :
null
}
</DescriptionList>
);
}
@@ -246,11 +287,12 @@ function HistoryDetails(props) {
/>
{
!!message &&
message ?
<DescriptionListItem
title="Message"
data={message}
/>
/> :
null
}
</DescriptionList>
);

View File

@@ -10,6 +10,12 @@
width: 80px;
}
.preferredWordScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
.releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css';

View File

@@ -1,5 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
@@ -194,6 +195,17 @@ class HistoryRow extends Component {
);
}
if (name === 'preferredWordScore') {
return (
<TableRowCell
key={name}
className={styles.preferredWordScore}
>
{formatPreferredWordScore(data.preferredWordScore)}
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell

View File

@@ -31,6 +31,8 @@ class Queue extends Component {
constructor(props, context) {
super(props, context);
this._shouldBlockRefresh = false;
this.state = {
allSelected: false,
allUnselected: false,
@@ -42,6 +44,14 @@ class Queue extends Component {
};
}
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
const {
items,
@@ -82,6 +92,10 @@ class Queue extends Component {
//
// Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
@@ -97,15 +111,19 @@ class Queue extends Component {
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
this.setState({ isConfirmRemoveModalOpen: true }, () => {
this._shouldBlockRefresh = true;
});
}
onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
}
onConfirmRemoveModalClose = () => {
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false });
}
@@ -205,7 +223,7 @@ class Queue extends Component {
}
{
isPopulated && !hasError && !items.length &&
isAllPopulated && !hasError && !items.length &&
<div>
Queue is empty
</div>
@@ -234,6 +252,7 @@ class Queue extends Component {
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
/>
);
})
@@ -260,6 +279,17 @@ class Queue extends Component {
return !!(item && item.seriesId && item.episodeId);
})
)}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>

View File

@@ -48,6 +48,7 @@ class QueueConnector extends Component {
const {
useCurrentPage,
fetchQueue,
fetchQueueStatus,
gotoQueueFirstPage
} = this.props;
@@ -58,6 +59,8 @@ class QueueConnector extends Component {
} else {
gotoQueueFirstPage();
}
fetchQueueStatus();
}
componentDidUpdate(prevProps) {
@@ -168,6 +171,7 @@ QueueConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchQueue: PropTypes.func.isRequired,
fetchQueueStatus: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired,
gotoQueuePreviousPage: PropTypes.func.isRequired,
gotoQueueNextPage: PropTypes.func.isRequired,

View File

@@ -19,6 +19,7 @@ import QueueStatusCell from './QueueStatusCell';
import TimeleftCell from './TimeleftCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import styles from './QueueRow.css';
import formatBytes from 'Utilities/Number/formatBytes';
class QueueRow extends Component {
@@ -41,20 +42,33 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true });
}
onRemoveQueueItemModalConfirmed = (blacklist) => {
this.props.onRemoveQueueItemPress(blacklist);
onRemoveQueueItemModalConfirmed = (blocklist) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist);
this.setState({ isRemoveQueueItemModalOpen: false });
}
onRemoveQueueItemModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isRemoveQueueItemModalOpen: false });
}
onInteractiveImportPress = () => {
this.props.onQueueRowModalOpenOrClose(true);
this.setState({ isInteractiveImportModalOpen: true });
}
onInteractiveImportModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isInteractiveImportModalOpen: false });
}
@@ -267,6 +281,12 @@ class QueueRow extends Component {
);
}
if (name === 'size') {
return (
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
@@ -357,6 +377,7 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!series}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
@@ -397,7 +418,8 @@ QueueRow.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired,
onRemoveQueueItemPress: PropTypes.func.isRequired
onRemoveQueueItemPress: PropTypes.func.isRequired,
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
};
QueueRow.defaultProps = {

View File

@@ -3,3 +3,7 @@
width: 30px;
}
.noMessages {
margin-bottom: 10px;
}

View File

@@ -12,7 +12,10 @@ function getDetailedPopoverBody(statusMessages) {
{
statusMessages.map(({ title, messages }) => {
return (
<div key={title}>
<div
key={title}
className={messages.length ? undefined: styles.noMessages}
>
{title}
<ul>
{

View File

@@ -21,7 +21,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blacklist: false
blocklist: false
};
}
@@ -31,7 +31,7 @@ class RemoveQueueItemModal extends Component {
resetState = function() {
this.setState({
remove: true,
blacklist: false
blocklist: false
});
}
@@ -42,8 +42,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
}
onRemoveConfirmed = () => {
@@ -65,10 +65,11 @@ class RemoveQueueItemModal extends Component {
const {
isOpen,
sourceTitle,
canIgnore
canIgnore,
isPending
} = this.props;
const { remove, blacklist } = this.state;
const { remove, blocklist } = this.state;
return (
<Modal
@@ -88,28 +89,32 @@ class RemoveQueueItemModal extends Component {
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormLabel>Add Release To Blocklist</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
name="blocklist"
value={blocklist}
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
onChange={this.onBlacklistChange}
onChange={this.onBlocklistChange}
/>
</FormGroup>
@@ -137,6 +142,7 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -22,7 +22,7 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blacklist: false
blocklist: false
};
}
@@ -32,7 +32,7 @@ class RemoveQueueItemsModal extends Component {
resetState = function() {
this.setState({
remove: true,
blacklist: false
blocklist: false
});
}
@@ -43,8 +43,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ remove: value });
}
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
}
onRemoveConfirmed = () => {
@@ -66,10 +66,11 @@ class RemoveQueueItemsModal extends Component {
const {
isOpen,
selectedCount,
canIgnore
canIgnore,
allPending
} = this.props;
const { remove, blacklist } = this.state;
const { remove, blocklist } = this.state;
return (
<Modal
@@ -89,30 +90,34 @@ class RemoveQueueItemsModal extends Component {
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div>
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
{
allPending ?
null :
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
Blacklist Release{selectedCount > 1 ? 's' : ''}
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
name="blocklist"
value={blocklist}
helpText="Prevents Sonarr from automatically grabbing this episode again"
onChange={this.onBlacklistChange}
onChange={this.onBlocklistChange}
/>
</FormGroup>
@@ -140,6 +145,7 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -154,7 +154,7 @@ class AddNewSeries extends Component {
<div className={styles.noResults}>Couldn't find any results for '{term}'</div>
<div>You can also search using TVDB ID of a show. eg. tvdb:71663</div>
<div>
<Link to="https://github.com/Sonarr/Sonarr/wiki/FAQ#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
<Link to="https://wiki.servarr.com/sonarr/faq#why-cant-i-add-a-new-series-when-i-know-the-tvdb-id">
Why can't I find my show?
</Link>
</div>

View File

@@ -30,9 +30,7 @@ class AddNewSeriesModalContent extends Component {
this.state = {
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
props.seriesType.value :
props.initialSeriesType,
searchForMissingEpisodes: false,
searchForCutoffUnmetEpisodes: false
props.initialSeriesType
};
}
@@ -45,14 +43,6 @@ class AddNewSeriesModalContent extends Component {
//
// Listeners
onSearchForMissingEpisodesChange = ({ value }) => {
this.setState({ searchForMissingEpisodes: value });
}
onSearchForCutoffUnmetEpisodesChange = ({ value }) => {
this.setState({ searchForCutoffUnmetEpisodes: value });
}
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
}
@@ -63,14 +53,10 @@ class AddNewSeriesModalContent extends Component {
onAddSeriesPress = () => {
const {
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
seriesType
} = this.state;
this.props.onAddSeriesPress(
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
seriesType
);
}
@@ -91,6 +77,8 @@ class AddNewSeriesModalContent extends Component {
languageProfileId,
seriesType,
seasonFolder,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
folder,
tags,
showLanguageProfile,
@@ -101,11 +89,6 @@ class AddNewSeriesModalContent extends Component {
...otherProps
} = this.props;
const {
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
@@ -271,8 +254,8 @@ class AddNewSeriesModalContent extends Component {
containerClassName={styles.searchInputContainer}
className={styles.searchInput}
name="searchForMissingEpisodes"
value={searchForMissingEpisodes}
onChange={this.onSearchForMissingEpisodesChange}
onChange={onInputChange}
{...searchForMissingEpisodes}
/>
</label>
@@ -285,8 +268,8 @@ class AddNewSeriesModalContent extends Component {
containerClassName={styles.searchInputContainer}
className={styles.searchInput}
name="searchForCutoffUnmetEpisodes"
value={searchForCutoffUnmetEpisodes}
onChange={this.onSearchForCutoffUnmetEpisodesChange}
onChange={onInputChange}
{...searchForCutoffUnmetEpisodes}
/>
</label>
</div>
@@ -319,6 +302,8 @@ AddNewSeriesModalContent.propTypes = {
languageProfileId: PropTypes.object,
seriesType: PropTypes.object.isRequired,
seasonFolder: PropTypes.object.isRequired,
searchForMissingEpisodes: PropTypes.object.isRequired,
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired,
showLanguageProfile: PropTypes.bool.isRequired,

View File

@@ -55,7 +55,7 @@ class AddNewSeriesModalContentConnector extends Component {
this.props.setAddSeriesDefault({ [name]: value });
}
onAddSeriesPress = (searchForMissingEpisodes, searchForCutoffUnmetEpisodes, seriesType) => {
onAddSeriesPress = (seriesType) => {
const {
tvdbId,
rootFolderPath,
@@ -63,6 +63,8 @@ class AddNewSeriesModalContentConnector extends Component {
qualityProfileId,
languageProfileId,
seasonFolder,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes,
tags
} = this.props;
@@ -74,9 +76,9 @@ class AddNewSeriesModalContentConnector extends Component {
languageProfileId: languageProfileId.value,
seriesType,
seasonFolder: seasonFolder.value,
tags: tags.value,
searchForMissingEpisodes,
searchForCutoffUnmetEpisodes
searchForMissingEpisodes: searchForMissingEpisodes.value,
searchForCutoffUnmetEpisodes: searchForCutoffUnmetEpisodes.value,
tags: tags.value
});
}
@@ -102,6 +104,8 @@ AddNewSeriesModalContentConnector.propTypes = {
languageProfileId: PropTypes.object,
seriesType: PropTypes.object.isRequired,
seasonFolder: PropTypes.object.isRequired,
searchForMissingEpisodes: PropTypes.object.isRequired,
searchForCutoffUnmetEpisodes: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired,
setAddSeriesDefault: PropTypes.func.isRequired,

View File

@@ -30,3 +30,9 @@
.importButtonIcon {
margin-right: 8px;
}
.addErrorAlert {
composes: alert from '~Components/Alert.css';
margin: 20px 0;
}

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { icons, kinds, sizes } from 'Helpers/Props';
import Button from 'Components/Link/Button';
import Alert from 'Components/Alert';
import Icon from 'Components/Icon';
import FieldSet from 'Components/FieldSet';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
@@ -47,21 +48,27 @@ class ImportSeriesSelectFolder extends Component {
isWindows,
isFetching,
isPopulated,
isSaving,
error,
saveError,
items
} = this.props;
const hasRootFolders = items.length > 0;
return (
<PageContent title="Import Series">
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
isFetching && !isPopulated ?
<LoadingIndicator /> :
null
}
{
!isFetching && !!error &&
<div>Unable to load root folders</div>
!isFetching && error ?
<div>Unable to load root folders</div> :
null
}
{
@@ -78,13 +85,16 @@ class ImportSeriesSelectFolder extends Component {
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>episode.s02e15.bluray.mkv</span>
</li>
<li className={styles.tip}>
Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span>
Point Sonarr to the folder containing all of your tv shows, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\tv shows' : '/tv shows'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\tv shows\\the simpsons' : '/tv shows/the simpsons'}"</span> Additionally, each series must be in its own folder within the root/library folder.
</li>
<li className={styles.tip}>
Do not use for importing downloads from your download client, this is only for existing organized libraries, not unsorted files.
</li>
</ul>
</div>
{
items.length > 0 ?
hasRootFolders ?
<div className={styles.recentFolders}>
<FieldSet legend="Root Folders">
<RootFolders
@@ -94,35 +104,51 @@ class ImportSeriesSelectFolder extends Component {
items={items}
/>
</FieldSet>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Choose another folder
</Button>
</div> :
<div className={styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
Start Import
</Button>
</div>
null
}
{
!isSaving && saveError ?
<Alert
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
Unable to add root folder
<ul>
{
saveError.responseJSON.map((e, index) => {
return (
<li key={index}>
{e.errorMessage}
</li>
);
})
}
</ul>
</Alert> :
null
}
<div className={hasRootFolders ? undefined : styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{
hasRootFolders ?
'Choose another folder' :
'Start Import'
}
</Button>
</div>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
@@ -142,7 +168,9 @@ ImportSeriesSelectFolder.propTypes = {
isWindows: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
error: PropTypes.object,
saveError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired
};

View File

@@ -13,7 +13,7 @@ import SeriesDetailsPageConnector from 'Series/Details/SeriesDetailsPageConnecto
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import MissingConnector from 'Wanted/Missing/MissingConnector';
import CutoffUnmetConnector from 'Wanted/CutoffUnmet/CutoffUnmetConnector';
import Settings from 'Settings/Settings';
@@ -118,8 +118,8 @@ function AppRoutes(props) {
/>
<Route
path="/activity/blacklist"
component={BlacklistConnector}
path="/activity/blocklist"
component={BlocklistConnector}
/>
{/*

View File

@@ -25,7 +25,7 @@
}
.time {
flex: 0 0 120px;
flex: 0 0 125px;
margin-right: 10px;
border: none !important;
}

View File

@@ -1,3 +1,5 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.event {
overflow-x: hidden;
margin: 4px 2px;
@@ -36,6 +38,11 @@
margin-left: 3px;
}
.statusContainer {
display: flex;
align-items: center;
}
.statusIcon {
margin-left: 3px;
}
@@ -51,6 +58,10 @@
.downloaded {
border-left-color: $successColor !important;
&:global(.fullColor) {
background-color: rgba(39, 194, 76, 0.4) !important;
}
&:global(.colorImpaired) {
border-left-color: color($successColor, saturation(+15%)) !important;
}
@@ -58,37 +69,73 @@
.downloading {
border-left-color: $purple !important;
&:global(.fullColor) {
background-color: rgba(122, 67, 182, 0.4) !important;
}
}
.unmonitored {
border-left-color: $gray !important;
&:global(.fullColor) {
background-color: rgba(173, 173, 173, 0.5) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.onAir {
border-left-color: $warningColor !important;
&:global(.fullColor) {
background-color: rgba(255, 165, 0, 0.6) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.missing {
border-left-color: $dangerColor !important;
&:global(.fullColor) {
background-color: rgba(240, 80, 80, 0.6) !important;
}
&:global(.colorImpaired) {
border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}
.unaired {
border-left-color: $primaryColor !important;
&:global(.fullColor) {
background-color: rgba(93, 156, 236, 0.4) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
}

View File

@@ -1,6 +1,6 @@
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import classNames from 'classnames';
import { icons, kinds } from 'Helpers/Props';
import formatTime from 'Utilities/Date/formatTime';
@@ -62,6 +62,7 @@ class CalendarEvent extends Component {
showFinaleIcon,
showSpecialIcon,
showCutoffUnmetIcon,
fullColorEvents,
timeFormat,
colorImpairedMode
} = this.props;
@@ -80,12 +81,13 @@ class CalendarEvent extends Component {
const seasonStatistics = season.statistics || {};
return (
<div>
<Fragment>
<Link
className={classNames(
styles.event,
styles[statusStyle],
colorImpairedMode && 'colorImpaired'
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
)}
component="div"
onPress={this.onPress}
@@ -95,95 +97,107 @@ class CalendarEvent extends Component {
{series.title}
</div>
{
missingAbsoluteNumber &&
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Episode does not have an absolute episode number"
/>
}
<div className={styles.statusContainer}>
{
missingAbsoluteNumber ?
<Icon
className={styles.statusIcon}
name={icons.WARNING}
title="Episode does not have an absolute episode number"
/> :
null
}
{
!!queueItem &&
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span>
}
{
queueItem ?
<span className={styles.statusIcon}>
<CalendarEventQueueDetails
{...queueItem}
/>
</span> :
null
}
{
!queueItem && grabbed &&
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title="Episode is downloading"
/>
}
{
!queueItem && grabbed ?
<Icon
className={styles.statusIcon}
name={icons.DOWNLOADING}
title="Episode is downloading"
/> :
null
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={kinds.WARNING}
title="Quality cutoff has not been met"
/>
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.qualityCutoffNotMet ?
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title="Quality cutoff has not been met"
/> :
null
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.languageCutoffNotMet &&
!episodeFile.qualityCutoffNotMet &&
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={kinds.WARNING}
title="Language cutoff has not been met"
/>
}
{
showCutoffUnmetIcon &&
!!episodeFile &&
episodeFile.languageCutoffNotMet &&
!episodeFile.qualityCutoffNotMet ?
<Icon
className={styles.statusIcon}
name={icons.EPISODE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title="Language cutoff has not been met"
/> :
null
}
{
episodeNumber === 1 && seasonNumber > 0 &&
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.INFO}
title={seasonNumber === 1 ? 'Series premiere' : 'Season premiere'}
/>
}
{
episodeNumber === 1 && seasonNumber > 0 ?
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.INFO}
darken={fullColorEvents}
title={seasonNumber === 1 ? 'Series premiere' : 'Season premiere'}
/> :
null
}
{
showFinaleIcon &&
episodeNumber !== 1 &&
seasonNumber > 0 &&
episodeNumber === seasonStatistics.totalEpisodeCount &&
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.WARNING}
title={series.status === 'ended' ? 'Series finale' : 'Season finale'}
/>
}
{
showFinaleIcon &&
episodeNumber !== 1 &&
seasonNumber > 0 &&
episodeNumber === seasonStatistics.totalEpisodeCount ?
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title={series.status === 'ended' ? 'Series finale' : 'Season finale'}
/> :
null
}
{
showSpecialIcon &&
(episodeNumber === 0 || seasonNumber === 0) &&
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.PINK}
title="Special"
/>
}
{
showSpecialIcon &&
(episodeNumber === 0 || seasonNumber === 0) ?
<Icon
className={styles.statusIcon}
name={icons.INFO}
kind={kinds.PINK}
darken={fullColorEvents}
title="Special"
/> :
null
}
</div>
</div>
{
showEpisodeInformation &&
showEpisodeInformation ?
<div className={styles.episodeInfo}>
<div className={styles.episodeTitle}>
{title}
@@ -193,11 +207,12 @@ class CalendarEvent extends Component {
{seasonNumber}x{padNumber(episodeNumber, 2)}
{
series.seriesType === 'anime' && absoluteEpisodeNumber &&
<span className={styles.absoluteEpisodeNumber}>({absoluteEpisodeNumber})</span>
series.seriesType === 'anime' && absoluteEpisodeNumber ?
<span className={styles.absoluteEpisodeNumber}>({absoluteEpisodeNumber})</span> : null
}
</div>
</div>
</div> :
null
}
<div className={styles.airTime}>
@@ -214,7 +229,7 @@ class CalendarEvent extends Component {
showOpenSeriesButton={true}
onModalClose={this.onDetailsModalClose}
/>
</div>
</Fragment>
);
}
}
@@ -236,6 +251,7 @@ CalendarEvent.propTypes = {
showFinaleIcon: PropTypes.bool.isRequired,
showSpecialIcon: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
onEventModalOpenToggle: PropTypes.func.isRequired

View File

@@ -74,6 +74,7 @@ class CalendarEventGroup extends Component {
showEpisodeInformation,
showFinaleIcon,
timeFormat,
fullColorEvents,
colorImpairedMode,
onEventModalOpenToggle
} = this.props;
@@ -133,7 +134,8 @@ class CalendarEventGroup extends Component {
className={classNames(
styles.eventGroup,
styles[statusStyle],
colorImpairedMode && 'colorImpaired'
colorImpairedMode && 'colorImpaired',
fullColorEvents && 'fullColor'
)}
>
<div className={styles.info}>
@@ -144,7 +146,7 @@ class CalendarEventGroup extends Component {
{
isMissingAbsoluteNumber &&
<Icon
className={styles.statusIcon}
containerClassName={styles.statusIcon}
name={icons.WARNING}
title="Episode does not have an absolute episode number"
/>
@@ -153,7 +155,7 @@ class CalendarEventGroup extends Component {
{
anyDownloading &&
<Icon
className={styles.statusIcon}
containerClassName={styles.statusIcon}
name={icons.DOWNLOADING}
title="An episode is downloading"
/>
@@ -162,9 +164,10 @@ class CalendarEventGroup extends Component {
{
firstEpisode.episodeNumber === 1 && seasonNumber > 0 &&
<Icon
className={styles.statusIcon}
containerClassName={styles.statusIcon}
name={icons.INFO}
kind={kinds.INFO}
darken={fullColorEvents}
title={seasonNumber === 1 ? 'Series Premiere' : 'Season Premiere'}
/>
}
@@ -175,9 +178,9 @@ class CalendarEventGroup extends Component {
seasonNumber > 0 &&
lastEpisode.episodeNumber === series.seasons.find((season) => season.seasonNumber === seasonNumber).statistics.totalEpisodeCount &&
<Icon
className={styles.statusIcon}
containerClassName={styles.statusIcon}
name={icons.INFO}
kind={kinds.WARNING}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
title={series.status === 'ended' ? 'Series finale' : 'Season finale'}
/>
}
@@ -237,6 +240,7 @@ CalendarEventGroup.propTypes = {
isDownloading: PropTypes.bool.isRequired,
showEpisodeInformation: PropTypes.bool.isRequired,
showFinaleIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired,
onEventModalOpenToggle: PropTypes.func.isRequired

View File

@@ -7,19 +7,23 @@ import styles from './Legend.css';
function Legend(props) {
const {
view,
showFinaleIcon,
showSpecialIcon,
showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode
} = props;
const iconsToShow = [];
const isAgendaView = view === 'agenda';
if (showFinaleIcon) {
iconsToShow.push(
<LegendIconItem
name="Finale"
icon={icons.INFO}
kind={kinds.WARNING}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
tooltip="Series or season finale"
/>
);
@@ -31,6 +35,7 @@ function Legend(props) {
name="Special"
icon={icons.INFO}
kind={kinds.PINK}
darken={fullColorEvents}
tooltip="Special episode"
/>
);
@@ -41,7 +46,7 @@ function Legend(props) {
<LegendIconItem
name="Cutoff Not Met"
icon={icons.EPISODE_FILE}
kind={kinds.WARNING}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
tooltip="Quality or language cutoff has not been met"
/>
);
@@ -53,12 +58,16 @@ function Legend(props) {
<LegendItem
status="unaired"
tooltip="Episode hasn't aired yet"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="unmonitored"
tooltip="Episode is unmonitored"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -68,12 +77,16 @@ function Legend(props) {
status="onAir"
name="On Air"
tooltip="Episode is currently airing"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="missing"
tooltip="Episode has aired and is missing from disk"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -82,12 +95,16 @@ function Legend(props) {
<LegendItem
status="downloading"
tooltip="Episode is currently downloading"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
<LegendItem
status="downloaded"
tooltip="Episode was downloaded and sorted"
isAgendaView={isAgendaView}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode}
/>
</div>
@@ -97,6 +114,7 @@ function Legend(props) {
name="Premiere"
icon={icons.INFO}
kind={kinds.INFO}
darken={true}
tooltip="Series or season premiere"
/>
@@ -115,9 +133,11 @@ function Legend(props) {
}
Legend.propTypes = {
view: PropTypes.string.isRequired,
showFinaleIcon: PropTypes.bool.isRequired,
showSpecialIcon: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -6,10 +6,12 @@ import Legend from './Legend';
function createMapStateToProps() {
return createSelector(
(state) => state.calendar.options,
(state) => state.calendar.view,
createUISettingsSelector(),
(calendarOptions, uiSettings) => {
(calendarOptions, view, uiSettings) => {
return {
...calendarOptions,
view,
colorImpairedMode: uiSettings.enableColorImpairedMode
};
}

View File

@@ -8,6 +8,7 @@ function LegendIconItem(props) {
name,
icon,
kind,
darken,
tooltip
} = props;
@@ -19,6 +20,7 @@ function LegendIconItem(props) {
<Icon
className={styles.icon}
name={icon}
darken={darken}
kind={kind}
/>
@@ -31,7 +33,12 @@ LegendIconItem.propTypes = {
name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired
};
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem;

View File

@@ -9,6 +9,8 @@ function LegendItem(props) {
name,
status,
tooltip,
isAgendaView,
fullColorEvents,
colorImpairedMode
} = props;
@@ -17,7 +19,8 @@ function LegendItem(props) {
className={classNames(
styles.legendItem,
styles[status],
colorImpairedMode && 'colorImpaired'
colorImpairedMode && 'colorImpaired',
fullColorEvents && !isAgendaView && 'fullColor'
)}
title={tooltip}
>
@@ -30,6 +33,8 @@ LegendItem.propTypes = {
name: PropTypes.string,
status: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
isAgendaView: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};

View File

@@ -25,14 +25,16 @@ class CalendarOptionsModalContent extends Component {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
enableColorImpairedMode,
fullColorEvents
} = props;
this.state = {
firstDayOfWeek,
calendarWeekColumnHeader,
timeFormat,
enableColorImpairedMode
enableColorImpairedMode,
fullColorEvents
};
}
@@ -96,6 +98,7 @@ class CalendarOptionsModalContent extends Component {
showFinaleIcon,
showSpecialIcon,
showCutoffUnmetIcon,
fullColorEvents,
onModalClose
} = this.props;
@@ -174,6 +177,18 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onOptionInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Full Color Events</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText="Altered style to color the entire event with the status color, instead of just the left edge. Does not apply to Agenda"
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
@@ -214,7 +229,9 @@ class CalendarOptionsModalContent extends Component {
value={timeFormat}
onChange={this.onGlobalInputChange}
/>
</FormGroup><FormGroup>
</FormGroup>
<FormGroup>
<FormLabel>Enable Color-Impaired Mode</FormLabel>
<FormInputGroup
@@ -225,7 +242,6 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onGlobalInputChange}
/>
</FormGroup>
</Form>
</FieldSet>
</ModalBody>
@@ -250,6 +266,7 @@ CalendarOptionsModalContent.propTypes = {
calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired

View File

@@ -1,7 +1,7 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_BLOCKLIST = 'ClearBlocklist';
export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles';

View File

@@ -16,4 +16,9 @@
color: #3a3f51;
font-size: 21px;
line-height: inherit;
&.small {
color: #909293;
font-size: 18px;
}
}

View File

@@ -1,5 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import classNames from 'classnames';
import { sizes } from 'Helpers/Props';
import styles from './FieldSet.css';
class FieldSet extends Component {
@@ -9,13 +11,14 @@ class FieldSet extends Component {
render() {
const {
size,
legend,
children
} = this.props;
return (
<fieldset className={styles.fieldSet}>
<legend className={styles.legend}>
<legend className={classNames(styles.legend, (size === sizes.SMALL) && styles.small)}>
{legend}
</legend>
{children}
@@ -26,8 +29,13 @@ class FieldSet extends Component {
}
FieldSet.propTypes = {
size: PropTypes.oneOf(sizes.all).isRequired,
legend: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
children: PropTypes.node
};
FieldSet.defaultProps = {
size: sizes.MEDIUM
};
export default FieldSet;

View File

@@ -128,7 +128,7 @@ class FileBrowserModalContent extends Component {
className={styles.mappedDrivesWarning}
kind={kinds.WARNING}
>
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://github.com/Sonarr/Sonarr/wiki/FAQ">FAQ</Link> for more information.
Mapped network drives are not available when running as a Windows Service, see the <Link className={styles.faqLink} to="https://wiki.servarr.com/sonarr/faq#why-cant-sonarr-see-my-files-on-a-remote-server">FAQ</Link> for more information.
</Alert>
}

View File

@@ -12,6 +12,7 @@ import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue';
import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue';
import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector';
import styles from './FilterBuilderRow.css';
@@ -75,6 +76,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.SERIES_STATUS:
return SeriesStatusFilterBuilderRowValue;
case filterBuilderValueTypes.SERIES_TYPES:
return SeriesTypeFilterBuilderRowValue;
case filterBuilderValueTypes.TAG:
return TagFilterBuilderRowValueConnector;

View File

@@ -16,7 +16,7 @@ function createTagListSelector() {
(selectedFilterBuilderProp.type === filterBuilderTypes.NUMBER ||
selectedFilterBuilderProp.type === filterBuilderTypes.STRING) &&
filterType !== filterTypes.EQUAL &&
filterType !== filterBuilderTypes.NOT_EQUAL ||
filterType !== filterTypes.NOT_EQUAL ||
!selectedFilterBuilderProp.optionsSelector
) {
return [];

View File

@@ -1,5 +1,5 @@
.tag {
height: 21px;
display: flex;
&.isLastTag {
.or {
@@ -18,4 +18,5 @@
.or {
margin: 0 3px;
color: $themeDarkColor;
line-height: 31px;
}

View File

@@ -6,7 +6,7 @@ import styles from './FilterBuilderRowValueTag.css';
function FilterBuilderRowValueTag(props) {
return (
<span
<div
className={styles.tag}
>
<TagInputTag
@@ -15,12 +15,13 @@ function FilterBuilderRowValueTag(props) {
/>
{
!props.isLastTag &&
<span className={styles.or}>
props.isLastTag ?
null :
<div className={styles.or}>
or
</span>
</div>
}
</span>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const seriesTypeList = [
{ id: 'anime', name: 'Anime' },
{ id: 'daily', name: 'Daily' },
{ id: 'standard', name: 'Standard' }
];
function SeriesTypeFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={seriesTypeList}
{...props}
/>
);
}
export default SeriesTypeFilterBuilderRowValue;

View File

@@ -5,6 +5,10 @@
align-items: center;
}
.editableContainer {
width: 100%;
}
.hasError {
composes: hasError from '~Components/Form/Input.css';
}
@@ -22,6 +26,16 @@
margin-left: 12px;
}
.dropdownArrowContainerEditable {
position: absolute;
top: 0;
right: 0;
padding-right: 17px;
width: 30%;
height: 35px;
text-align: right;
}
.dropdownArrowContainerDisabled {
composes: dropdownArrowContainer;
@@ -71,3 +85,21 @@
display: inline-block;
margin: 5px -5px 5px 0;
}
.mobileCloseButtonContainer {
display: flex;
justify-content: flex-end;
height: 40px;
border-bottom: 1px solid $borderColor;
}
.mobileCloseButton {
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
&:hover {
color: $modalCloseButtonHoverColor;
}
}

View File

@@ -15,6 +15,7 @@ import Measure from 'Components/Measure';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import Scroller from 'Components/Scroller/Scroller';
import TextInput from './TextInput';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import HintedSelectInputOption from './HintedSelectInputOption';
import styles from './EnhancedSelectInput.css';
@@ -169,11 +170,21 @@ class EnhancedSelectInput extends Component {
}
}
onFocus = () => {
if (this.state.isOpen) {
this._removeListener();
this.setState({ isOpen: false });
}
}
onBlur = () => {
// 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 });
if (!this.props.isEditable) {
// 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 });
}
}
}
@@ -297,16 +308,19 @@ class EnhancedSelectInput extends Component {
const {
className,
disabledClassName,
name,
value,
values,
isDisabled,
isEditable,
isFetching,
hasError,
hasWarning,
valueOptions,
selectedValueOptions,
selectedValueComponent: SelectedValueComponent,
optionComponent: OptionComponent
optionComponent: OptionComponent,
onChange
} = this.props;
const {
@@ -332,52 +346,94 @@ class EnhancedSelectInput extends Component {
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
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
{
isEditable ?
<div
className={styles.editableContainer}
>
<TextInput
className={className}
name={name}
value={value}
readOnly={isDisabled}
hasError={hasError}
hasWarning={hasWarning}
onFocus={this.onFocus}
onBlur={this.onBlur}
onChange={onChange}
/>
<Link
className={classNames(
styles.dropdownArrowContainerEditable,
isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer)
}
onPress={this.onPress}
>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</Link>
</div> :
<Link
className={classNames(
className,
hasError && styles.hasError,
hasWarning && styles.hasWarning,
isDisabled && disabledClassName
)}
isDisabled={isDisabled}
onBlur={this.onBlur}
onKeyDown={this.onKeyDown}
onPress={this.onPress}
>
<SelectedValueComponent
value={value}
values={values}
{...selectedValueOptions}
{...selectedOption}
isDisabled={isDisabled}
isMultiSelect={isMultiSelect}
>
{selectedOption ? selectedOption.value : null}
</SelectedValueComponent>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
<div
className={isDisabled ?
styles.dropdownArrowContainerDisabled :
styles.dropdownArrowContainer
}
>
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</div>
</Link>
{
isFetching &&
<LoadingIndicator
className={styles.loading}
size={20}
/>
}
{
!isFetching &&
<Icon
name={icons.CARET_DOWN}
/>
}
</div>
</Link>
}
</Measure>
</div>
)}
@@ -462,6 +518,18 @@ class EnhancedSelectInput extends Component {
scrollDirection={scrollDirections.NONE}
>
<Scroller className={styles.optionsModalScroller}>
<div className={styles.mobileCloseButtonContainer}>
<Link
className={styles.mobileCloseButton}
onPress={this.onOptionsModalClose}
>
<Icon
name={icons.CLOSE}
size={18}
/>
</Link>
</div>
{
values.map((v, index) => {
const hasParent = v.parentKey !== undefined;
@@ -502,6 +570,7 @@ EnhancedSelectInput.propTypes = {
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isEditable: PropTypes.bool.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
valueOptions: PropTypes.object.isRequired,
@@ -517,6 +586,7 @@ EnhancedSelectInput.defaultProps = {
disabledClassName: styles.isDisabled,
isDisabled: false,
isFetching: false,
isEditable: false,
valueOptions: {},
selectedValueOptions: {},
selectedValueComponent: HintedSelectInputSelectedValue,

View File

@@ -54,4 +54,8 @@
&:last-child {
border: none;
}
&:hover {
background-color: unset;
}
}

View File

@@ -12,7 +12,9 @@ class EnhancedSelectInputOption extends Component {
//
// Listeners
onPress = () => {
onPress = (e) => {
e.preventDefault();
const {
id,
onSelect

View File

@@ -20,8 +20,10 @@ import SeriesTypeSelectInput from './SeriesTypeSelectInput';
import EnhancedSelectInput from './EnhancedSelectInput';
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
import TagInputConnector from './TagInputConnector';
import TagSelectInputConnector from './TagSelectInputConnector';
import TextTagInputConnector from './TextTagInputConnector';
import TextInput from './TextInput';
import UMaskInput from './UMaskInput';
import FormInputHelpText from './FormInputHelpText';
import styles from './FormInputGroup.css';
@@ -84,6 +86,12 @@ function getComponent(type) {
case inputTypes.TEXT_TAG:
return TextTagInputConnector;
case inputTypes.TAG_SELECT:
return TagSelectInputConnector;
case inputTypes.UMASK:
return UMaskInput;
default:
return TextInput;
}
@@ -191,7 +199,7 @@ function FormInputGroup(props) {
}
{
!checkInput && helpTextWarning &&
(!checkInput || helpText) && helpTextWarning &&
<FormInputHelpText
text={helpTextWarning}
isWarning={true}

View File

@@ -3,10 +3,17 @@ import React from 'react';
import TextInput from './TextInput';
import styles from './PasswordInput.css';
// Prevent a user from copying (or cutting) the password from the input
function onCopy(e) {
e.preventDefault();
e.nativeEvent.stopImmediatePropagation();
}
function PasswordInput(props) {
return (
<TextInput
{...props}
onCopy={onCopy}
/>
);
}

View File

@@ -29,6 +29,8 @@ function getType({ type, selectOptionsProviderAction }) {
return inputTypes.SELECT;
case 'tag':
return inputTypes.TEXT_TAG;
case 'tagSelect':
return inputTypes.TAG_SELECT;
case 'textbox':
return inputTypes.TEXT;
case 'oAuth':

View File

@@ -10,13 +10,16 @@ const ADD_NEW_KEY = 'addNew';
function createMapStateToProps() {
return createSelector(
(state) => state.rootFolders,
(state, { value }) => value,
(state, { includeMissingValue }) => includeMissingValue,
(state, { includeNoChange }) => includeNoChange,
(rootFolders, includeNoChange) => {
(rootFolders, value, includeMissingValue, includeNoChange) => {
const values = rootFolders.items.map((rootFolder) => {
return {
key: rootFolder.path,
value: rootFolder.path,
freeSpace: rootFolder.freeSpace
freeSpace: rootFolder.freeSpace,
isMissing: false
};
});
@@ -24,7 +27,8 @@ function createMapStateToProps() {
values.unshift({
key: 'noChange',
value: 'No Change',
isDisabled: true
isDisabled: true,
isMissing: false
});
}
@@ -37,6 +41,15 @@ function createMapStateToProps() {
});
}
if (includeMissingValue && !values.find((v) => v.key === value)) {
values.push({
key: value,
value,
isMissing: true,
isDisabled: true
});
}
values.push({
key: ADD_NEW_KEY,
value: 'Add a new path'

View File

@@ -27,3 +27,9 @@
color: $darkGray;
font-size: $smallFontSize;
}
.isMissing {
margin-left: 15px;
color: $dangerColor;
font-size: $smallFontSize;
}

View File

@@ -10,6 +10,7 @@ function RootFolderSelectInputOption(props) {
id,
value,
freeSpace,
isMissing,
seriesFolder,
isMobile,
isWindows,
@@ -43,11 +44,20 @@ function RootFolderSelectInputOption(props) {
</div>
{
freeSpace != null &&
freeSpace == null ?
null :
<div className={styles.freeSpace}>
{formatBytes(freeSpace)} Free
</div>
}
{
isMissing ?
<div className={styles.isMissing}>
Missing
</div> :
null
}
</div>
</EnhancedSelectInputOption>
);
@@ -57,6 +67,7 @@ RootFolderSelectInputOption.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
freeSpace: PropTypes.number,
isMissing: PropTypes.boolean,
seriesFolder: PropTypes.string,
isMobile: PropTypes.bool.isRequired,
isWindows: PropTypes.bool

View File

@@ -75,6 +75,12 @@ class TagInput extends Component {
//
// Listeners
onTagEdit = ({ value, ...otherProps }) => {
this.setState({ value });
this.props.onTagDelete(otherProps);
}
onInputContainerPress = () => {
this._autosuggestRef.input.focus();
}
@@ -188,6 +194,7 @@ class TagInput extends Component {
const {
tags,
kind,
canEdit,
tagComponent,
onTagDelete
} = this.props;
@@ -199,8 +206,10 @@ class TagInput extends Component {
kind={kind}
inputProps={inputProps}
isFocused={this.state.isFocused}
canEdit={canEdit}
tagComponent={tagComponent}
onTagDelete={onTagDelete}
onTagEdit={this.onTagEdit}
onInputContainerPress={this.onInputContainerPress}
/>
);
@@ -262,6 +271,7 @@ TagInput.propTypes = {
placeholder: PropTypes.string.isRequired,
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
minQueryLength: PropTypes.number.isRequired,
canEdit: PropTypes.bool,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
tagComponent: PropTypes.elementType.isRequired,
@@ -277,6 +287,7 @@ TagInput.defaultProps = {
placeholder: '',
delimiters: ['Tab', 'Enter', ' ', ','],
minQueryLength: 1,
canEdit: false,
tagComponent: TagInputTag
};

View File

@@ -28,8 +28,10 @@ class TagInputInput extends Component {
tags,
inputProps,
kind,
canEdit,
tagComponent: TagComponent,
onTagDelete
onTagDelete,
onTagEdit
} = this.props;
return (
@@ -47,8 +49,10 @@ class TagInputInput extends Component {
index={index}
tag={tag}
kind={kind}
canEdit={canEdit}
isLastTag={index === tags.length - 1}
onDelete={onTagDelete}
onEdit={onTagEdit}
/>
);
})
@@ -67,8 +71,10 @@ TagInputInput.propTypes = {
inputProps: PropTypes.object.isRequired,
kind: PropTypes.oneOf(kinds.all).isRequired,
isFocused: PropTypes.bool.isRequired,
canEdit: PropTypes.bool.isRequired,
tagComponent: PropTypes.elementType.isRequired,
onTagDelete: PropTypes.func.isRequired,
onTagEdit: PropTypes.func.isRequired,
onInputContainerPress: PropTypes.func.isRequired
};

View File

@@ -1,5 +1,19 @@
.tag {
composes: link from '~Components/Link/Link.css';
display: flex;
justify-content: center;
flex-direction: column;
height: 31px;
}
.editContainer {
display: inline-block;
margin-left: 4px;
padding-left: 2px;
border-left: 1px solid #eee;
}
.editButton {
composes: button from '~Components/Link/IconButton.css';
width: auto;
}

View File

@@ -1,8 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import { icons, kinds } from 'Helpers/Props';
import tagShape from 'Helpers/Props/Shapes/tagShape';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import styles from './TagInputTag.css';
@@ -24,24 +25,59 @@ class TagInputTag extends Component {
});
}
onEdit = () => {
const {
index,
tag,
onEdit
} = this.props;
onEdit({
index,
id: tag.id,
value: tag.name
});
}
//
// Render
render() {
const {
tag,
kind
kind,
canEdit
} = this.props;
return (
<Link
<div
className={styles.tag}
tabIndex={-1}
onPress={this.onDelete}
>
<Label kind={kind}>
{tag.name}
<Label
kind={kind}
>
<Link
tabIndex={-1}
onPress={this.onDelete}
>
{tag.name}
</Link>
{
canEdit ?
<div className={styles.editContainer}>
<IconButton
className={styles.editButton}
name={icons.EDIT}
size={9}
onPress={this.onEdit}
/>
</div> :
null
}
</Label>
</Link>
</div>
);
}
}
@@ -50,7 +86,9 @@ TagInputTag.propTypes = {
index: PropTypes.number.isRequired,
tag: PropTypes.shape(tagShape),
kind: PropTypes.oneOf(kinds.all).isRequired,
onDelete: PropTypes.func.isRequired
canEdit: PropTypes.bool.isRequired,
onDelete: PropTypes.func.isRequired,
onEdit: PropTypes.func.isRequired
};
export default TagInputTag;

View File

@@ -0,0 +1,102 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import TagInput from './TagInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state, { values }) => values,
(tags, tagList) => {
const sortedTags = _.sortBy(tagList, 'value');
return {
tags: tags.reduce((acc, tag) => {
const matchingTag = _.find(tagList, { key: tag });
if (matchingTag) {
acc.push({
id: tag,
name: matchingTag.value
});
}
return acc;
}, []),
tagList: sortedTags.map(({ key: id, value: name }) => {
return {
id,
name
};
}),
allTags: sortedTags
};
}
);
}
class TagSelectInputConnector extends Component {
//
// Listeners
onTagAdd = (tag) => {
const {
name,
value,
allTags
} = this.props;
const existingTag =_.some(allTags, { key: tag.id });
const newValue = value.slice();
if (existingTag) {
newValue.push(tag.id);
}
this.props.onChange({ name, value: newValue });
}
onTagDelete = ({ index }) => {
const {
name,
value
} = this.props;
const newValue = value.slice();
newValue.splice(index, 1);
this.props.onChange({
name,
value: newValue
});
}
//
// Render
render() {
return (
<TagInput
onTagAdd={this.onTagAdd}
onTagDelete={this.onTagDelete}
{...this.props}
/>
);
}
}
TagSelectInputConnector.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.arrayOf(PropTypes.number).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
allTags: PropTypes.arrayOf(PropTypes.object).isRequired,
onChange: PropTypes.func.isRequired
};
export default connect(createMapStateToProps)(TagSelectInputConnector);

View File

@@ -130,7 +130,8 @@ class TextInput extends Component {
step,
min,
max,
onBlur
onBlur,
onCopy
} = this.props;
return (
@@ -155,6 +156,8 @@ class TextInput extends Component {
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={onBlur}
onCopy={onCopy}
onCut={onCopy}
onKeyUp={this.onKeyUp}
onMouseDown={this.onMouseDown}
onMouseUp={this.onMouseUp}
@@ -180,6 +183,7 @@ TextInput.propTypes = {
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
onCopy: PropTypes.func,
onSelectionChange: PropTypes.func
};

View File

@@ -0,0 +1,53 @@
.inputWrapper {
display: flex;
}
.inputFolder {
composes: input from '~Components/Form/Input.css';
max-width: 100px;
}
.inputUnitWrapper {
position: relative;
width: 100%;
}
.inputUnit {
composes: inputUnit from '~Components/Form/FormInputGroup.css';
right: 40px;
font-family: $monoSpaceFontFamily;
}
.unit {
font-family: $monoSpaceFontFamily;
}
.details {
margin-top: 5px;
margin-left: 17px;
line-height: 20px;
> div {
display: flex;
label {
flex: 0 0 50px;
}
.value {
width: 50px;
text-align: right;
}
.unit {
width: 90px;
text-align: right;
}
}
}
.readOnly {
background-color: #eee;
}

View File

@@ -0,0 +1,133 @@
/* eslint-disable no-bitwise */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styles from './UMaskInput.css';
import EnhancedSelectInput from './EnhancedSelectInput';
const umaskOptions = [
{
key: '755',
value: '755 - Owner write, Everyone else read',
hint: 'drwxr-xr-x'
},
{
key: '775',
value: '775 - Owner & Group write, Other read',
hint: 'drwxrwxr-x'
},
{
key: '770',
value: '770 - Owner & Group write',
hint: 'drwxrwx---'
},
{
key: '750',
value: '750 - Owner write, Group read',
hint: 'drwxr-x---'
},
{
key: '777',
value: '777 - Everyone write',
hint: 'drwxrwxrwx'
}
];
function formatPermissions(permissions) {
const hasSticky = permissions & 0o1000;
const hasSetGID = permissions & 0o2000;
const hasSetUID = permissions & 0o4000;
let result = '';
for (let i = 0; i < 9; i++) {
const bit = (permissions & (1 << i)) !== 0;
let digit = bit ? 'xwr'[i % 3] : '-';
if (i === 6 && hasSetUID) {
digit = bit ? 's' : 'S';
} else if (i === 3 && hasSetGID) {
digit = bit ? 's' : 'S';
} else if (i === 0 && hasSticky) {
digit = bit ? 't' : 'T';
}
result = digit + result;
}
return result;
}
class UMaskInput extends Component {
//
// Render
render() {
const {
name,
value,
onChange
} = this.props;
const valueNum = parseInt(value, 8);
const umaskNum = 0o777 & ~valueNum;
const umask = umaskNum.toString(8).padStart(4, '0');
const folderNum = 0o777 & ~umaskNum;
const folder = folderNum.toString(8).padStart(3, '0');
const fileNum = 0o666 & ~umaskNum;
const file = fileNum.toString(8).padStart(3, '0');
const unit = formatPermissions(folderNum);
const values = umaskOptions.map((v) => {
return { ...v, hint: <span className={styles.unit}>{v.hint}</span> };
});
return (
<div>
<div className={styles.inputWrapper}>
<div className={styles.inputUnitWrapper}>
<EnhancedSelectInput
name={name}
value={value}
values={values}
isEditable={true}
onChange={onChange}
/>
<div className={styles.inputUnit}>
d{unit}
</div>
</div>
</div>
<div className={styles.details}>
<div>
<label>UMask</label>
<div className={styles.value}>{umask}</div>
</div>
<div>
<label>Folder</label>
<div className={styles.value}>{folder}</div>
<div className={styles.unit}>d{formatPermissions(folderNum)}</div>
</div>
<div>
<label>File</label>
<div className={styles.value}>{file}</div>
<div className={styles.unit}>{formatPermissions(fileNum)}</div>
</div>
</div>
</div>
);
}
}
UMaskInput.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func
};
export default UMaskInput;

View File

@@ -2,3 +2,7 @@
margin-right: 5px;
color: $themeRed;
}
.rating {
margin-right: 15px;
}

View File

@@ -6,7 +6,7 @@ import styles from './HeartRating.css';
function HeartRating({ rating, iconSize }) {
return (
<span>
<span className={styles.rating}>
<Icon
className={styles.heart}
name={icons.HEART}

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