Compare commits

..

124 Commits

Author SHA1 Message Date
Robin Dadswell
d0952d4a0c fixed small test issue 2021-01-19 19:06:48 -05:00
Robin Dadswell
579f1eae2d fixed health check selector 2021-01-19 19:06:48 -05:00
Robin Dadswell
091ebb5cee more linting fixes 2021-01-19 19:06:48 -05:00
Robin Dadswell
d3a96fac12 added missing css file 2021-01-19 19:06:47 -05:00
Robin Dadswell
da0b60dd96 Updated URL to readarr not lidarr 2021-01-19 19:06:47 -05:00
ta264
3bfdd4aafa New: Warn if UI won't update due to SignalR errors
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:47 -05:00
Robin Dadswell
da2bc75eab Most linting fixes completed 2021-01-19 19:06:47 -05:00
Robin Dadswell
1d922f3ce6 final backend fix 2021-01-19 19:06:47 -05:00
Robin Dadswell
b3bdaeeffa fixed RootFolderChecks 2021-01-19 19:06:47 -05:00
Qstick
604c354284 New: Event Driven HealthCheck Support
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:47 -05:00
Qstick
ec0fc6f3e1 Fixed: Edge case where import fails due to DB relationship mismatch
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:47 -05:00
Robin Dadswell
2d28359627 more backend build fixes 2021-01-19 19:06:47 -05:00
Robin Dadswell
45d0f4bdb7 fixes to build errors 2021-01-19 19:06:47 -05:00
Robin Dadswell
ba1f2a5a7b more build fixes 2021-01-19 19:06:47 -05:00
Robin Dadswell
0ef8f2b2f7 finished fixing IsUpgradable 2021-01-19 19:06:47 -05:00
Robin Dadswell
91d6fe90cd minor fixes to build - half finished 2021-01-19 19:06:47 -05:00
Robin Dadswell
baa728f2c1 removed additional whitespace 2021-01-19 19:06:47 -05:00
Robin Dadswell
d20f9ea269 fixed seriesActions 2021-01-19 19:06:47 -05:00
Robin Dadswell
db809f579c removed get columns as no longer user 2021-01-19 19:06:47 -05:00
Robin Dadswell
4970ed5323 fixed authoreditor 2021-01-19 19:06:47 -05:00
Robin Dadswell
fa653bf546 health check selector fix 2021-01-19 19:06:47 -05:00
Robin Dadswell
f496d96907 removed striphtml (as per lidarr) 2021-01-19 19:06:47 -05:00
Robin Dadswell
cfc52d12ea move to PageContentBody from PageContentBodyConnector 2021-01-19 19:06:47 -05:00
Robin Dadswell
40f156c16e regenerated lock file 2021-01-19 19:06:47 -05:00
bakerboy448
a322d9ed6d lint fixes 2021-01-19 19:06:46 -05:00
ta264
604666f961 Add SortKey validation
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
851ff948af Fixed: False Positives for RemotePath check with Deluge
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
b6f052f39c Fixed: Show TLS errors in UI when testing download clients
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
e6236823cf Resource missing from Gotify call
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
32c759d32b Gotify token as query parameter
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
993d5818a4 Convert Notifications from RestSharp to HttpClient
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
2b93aaa9b2 Fixed: Manual Import Fails on failed Import Items
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
4ec873bf7a Fixed binary execute permissions for osx and Radarr
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
b574048733 Fixed disk permission tests
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
5c4dfc5e7b New: Displaying folder-based permissions in UI rather than file-based permissions and with selectable sane presets
Fixed: Preserve setgid when applying unix permissions
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
5545ae94ac Readded 0 cat to the end of the Newznab list
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
15e0b8dd4d Improve use of All() for Path related queries
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
ac4a82abf4 Mass Editor size and options
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
cb07186970 Fixed: Size on disk sorting and display
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
e5409dbff6 New: Differentiate between short term and long term (more than 6 hours) indexer failures
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
59d6a77b74 Fixed: (Windows) clean up extraneous files in build folder during installation
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
db0151c39b New: Bulk remove from Blacklist
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
4ac646685c New: Show .net version in UI
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
d01d112bfe Fixed: Cleanse account and passwd from Download Station URLs
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
77ef282916 Fixed: Webhooks using lower case event types (in the future this could change)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Mark McDowall
e5f483eadc New: Health events for Webhooks
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Qstick
8e78bf71a1 Fixed: Failing file copy when running in docker on synology with btrfs
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
0c581c6146 Fixed: Regression causing updater to fail (manual update required if on 3.0.3.971, see forums)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:46 -05:00
Taloth Saldono
cabec3c625 Fixed: Dataloss when moving series folder to root folder with only different casing
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth
a0ef1ebaad New: Newznab/Torznab categories dropdown with indexer provided category names
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
5abfee1bf8 Handle ratelimit api response for newznab caps endpoint on certain newznab indexers that have caps behind the apikey
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
f46d5534f4 Fixed: Preview rename tip wording
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
35225c799b Fixed: Artist/Album navigation buttons hidden with some titles
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
89584666ff Fixed: Links and already added icons overflowing on add artist/album search results
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
a776336b8c Fixed: Exception when parsing Quality in release title with colon
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
39a0cb3f43 Fixed: Long paths overflowing in artist history
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Mark McDowall
2abee1970f Don't process queue item without details
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Mark McDowall
cdf648670d Fixed: Show more information in UI when testing SAB fails in some cases
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Ryan
810a02090b Fixed: Typo/unclear text in backup retention
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
4282b84fb6 Remove stacktrace if hardlink resulted in EXDEV.
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
42acea55fd Fixed: Performance of symbolic link detection and infinite recursion
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
c7e7d48ca3 New: Fast copy using reflink on btrfs volumes
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Taloth Saldono
b1fbdebba9 Fixed: Removed hardlink-based transactional file transfer logic (instead relying on explicit copy+delete for cifs)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
b4bfd7d07f Fixed: Sorting of queue by artist title when unknown items are included
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
470ba66d4a Moved Windows-only Permission function to Lidarr.Windows
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:45 -05:00
Qstick
400fa4a960 fix modifiers for various classes
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Mark McDowall
e9de0420dd Fixed: Indexer being disabled due to download client rejecting it
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
56a623bad9 Added PrivacyLevel option to FieldDefinition for later usage
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
26bf0c0542 Added MultiSelect input control for provider settings
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
33de5d2049 New: Added FileList.io indexer support
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Arthur Bols
1e3b775837 New: Removed chown and simplified chmod options for linux/osx
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Taloth Saldono
efa77c1823 Allow inline markdown in the changelog for linking to wiki
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Qstick
3428ee3ca2 Fixed: Not removing seeded download if it was manual imported in some cases
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Mark McDowall
27c9337071 Fixed: Rejections custom filter for Interactive Search (now Rejections Count)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Qstick
1fbaefc054 Improve root folder health check
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Skyler Mäntysaari
b1cfa90a9f New: SendGrid Notifications
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:44 -05:00
Qstick
3e01c3089c Fixed: Added .org to website url filtering in parser
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
2e80c86e23 Fixed recursion issue when emptying recycle bin
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
31837958c1 Fixed: Tag details list series in alphabetical order
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
a22297c197 Added UserAgent to api request trace log
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
6e595bfad3 New: Add DownloadClient and DownloadId to Webhook notifications
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
8ffcc8a711 Clarify that Post-Import Category torrents are not monitored by Sonarr.
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
4b3db6f596 Fixed: Windows installer won't create shortcut if unchecked
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Taloth Saldono
778085544c Improved error message when nzb download contains an newznab error instead
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
81a20ab0e5 Fixed: Ended overlay on artist posters
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
770176e127 Rename FilterFiles to FilterPaths
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
2f56814f58 Fixed: Queue not always clearing checked items when updated
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
4247605dc9 Fixed: Remove website post fix before parsing
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Taloth Saldono
3d4c8df6f7 Linting error
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
a5e69528a9 Fix Release Push log statement
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Jacob
074bcb4ebf New: Added option to filter Release Profile to a specific indexer
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Mark McDowall
2bd4965763 New: Clone indexer button
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:43 -05:00
Qstick
fdef79a9dc Fixed: Manual Import sorting by quality
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
1423ebdb07 Fixed: Prompt to restart after resetting API key
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
06d3e47232 Fixed: Sorting by track count
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
255c19bb83 Added Norwegian Bokmal alias
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
4104efa9a0 Fixed: Representation of episode start time when not starting at the full hour in am/pm notation
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
5f1b928cd9 Fixed: RestClient does not use global proxy settings
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
49b085bac6 New: Limit recent folders in Manual import to 10 and descending order
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
5a4034ec6a Fix proptype warning for id of EnhancedSelectInputOption
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
ed1175ed86 Remove website prefixes with dashes in URL
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
31287eb0d7 Fixed: Details for episode history flashing on mobile devices
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Fossil
664905b4f2 Remove PFMonkey.com from Presets
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
a4223e8dbf Fixed: Test All not clearing health error
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
3af8c2f504 Improved some log messages
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
67086060b1 Fixed: Delete files from Artist Mass Editor not actually deleting files
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
099fd3166c Tiny fix in test, left-over from my on-windows test.
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Taloth Saldono
43363cbd08 Fixed: File imports on cloud drives slow due to transaction logic
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
7d0eeca9a9 Fixed: Use Proxy for MediaCovers and Metadata
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
aa7d289a48 Fixed: Set permissions on extra and subtitle files
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Mark McDowall
c0f8d92c6e Fixed: Include releases that failed to parse in search results
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
2732ea43ad New: Event Driven HealthCheck Support
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
bc059c3f32 Fixed: Disregard Real when user disabled proper preference
Co-Authored-By: Taloth <Taloth@users.noreply.github.com>
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
172f8e4b31 Simplify ManualImportModule null check
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
2e9087d4c3 Fixed: Edge case where import fails due to DB relationship mismatch
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
d004f95297 Fixed: Improved failed series search messaging
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
566d84e22d Fixed: Manage Tracks not showing whether language/quality meets cutoff
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
8dd3c14132 Fixed: Delay profile being ignored for non-revision upgrades
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
acb3470988 Remove unnecessary usings
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:42 -05:00
Qstick
953d8a92c2 Remove Dotnet Framework Version Checks
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
nitsua
129245cc5b New: Add label to disk usage progress bar
(cherry picked from commit 7c8ac300777583cb93d9deeed1328bcffaef555c)
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
Qstick
4ecd7eaf08 Update README and CONTRIBUTING
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
Qstick
698eedb51a Cache Yarn Packages on build
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
Qstick
a94623d95b Catchup Linting
Signed-off-by: Robin Dadswell <robin@dadswell.email>
2021-01-19 19:06:41 -05:00
2131 changed files with 38512 additions and 108872 deletions

View File

@@ -19,10 +19,10 @@ indent_size = 4
dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
# Indentation preferences
csharp_indent_block_contents = true
@@ -32,6 +32,10 @@ csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
@@ -106,13 +110,13 @@ dotnet_diagnostic.SA1643.severity = none
dotnet_diagnostic.SA1648.severity = none
dotnet_diagnostic.SA1649.severity = none
dotnet_diagnostic.SA1651.severity = none
dotnet_diagnostic.SX1101.severity = warning
dotnet_diagnostic.SX1309.severity = warning
# Microsoft Analyzers that fail and need to be sorted thru
dotnet_diagnostic.ASP0000.severity = suggestion
dotnet_diagnostic.CA1000.severity = suggestion
dotnet_diagnostic.CA1001.severity = suggestion
dotnet_diagnostic.CA1002.severity = suggestion
dotnet_diagnostic.CA1003.severity = suggestion
dotnet_diagnostic.CA1008.severity = suggestion
dotnet_diagnostic.CA1010.severity = suggestion
@@ -159,16 +163,10 @@ dotnet_diagnostic.CA1304.severity = suggestion
dotnet_diagnostic.CA1305.severity = suggestion
dotnet_diagnostic.CA1307.severity = suggestion
dotnet_diagnostic.CA1308.severity = suggestion
dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion
dotnet_diagnostic.CA1708.severity = suggestion
dotnet_diagnostic.CA1710.severity = suggestion
dotnet_diagnostic.CA1711.severity = suggestion
dotnet_diagnostic.CA1712.severity = suggestion
dotnet_diagnostic.CA1714.severity = suggestion
dotnet_diagnostic.CA1715.severity = suggestion
@@ -177,14 +175,12 @@ dotnet_diagnostic.CA1717.severity = suggestion
dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.severity = suggestion
dotnet_diagnostic.CA1801.severity = suggestion
dotnet_diagnostic.CA1802.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion
dotnet_diagnostic.CA1813.severity = suggestion
dotnet_diagnostic.CA1814.severity = suggestion
dotnet_diagnostic.CA1815.severity = suggestion
dotnet_diagnostic.CA1816.severity = suggestion
@@ -204,6 +200,9 @@ dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.severity = suggestion
dotnet_diagnostic.CA2009.severity = suggestion
dotnet_diagnostic.CA2010.severity = suggestion
dotnet_diagnostic.CA2011.severity = suggestion
dotnet_diagnostic.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion
@@ -211,7 +210,6 @@ dotnet_diagnostic.CA2101.severity = suggestion
dotnet_diagnostic.CA2119.severity = suggestion
dotnet_diagnostic.CA2153.severity = suggestion
dotnet_diagnostic.CA2200.severity = suggestion
dotnet_diagnostic.CA2201.severity = suggestion
dotnet_diagnostic.CA2207.severity = suggestion
dotnet_diagnostic.CA2208.severity = suggestion
dotnet_diagnostic.CA2211.severity = suggestion
@@ -257,8 +255,6 @@ dotnet_diagnostic.CA5374.severity = suggestion
dotnet_diagnostic.CA5379.severity = suggestion
dotnet_diagnostic.CA5384.severity = suggestion
dotnet_diagnostic.CA5385.severity = suggestion
dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion

35
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,35 @@
---
name: Bug Report
about: Support Requests will be closed immediately, if you are unsure go to our Reddit or Discord first. Exceptions do not mean you found a bug!
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Platform Information (please complete the following information):**
- OS: [e.g. Windows 10 2004 / Ubuntu 20.10]
- Docker: [Yes/No]
- Mono or.NET Core Version: [e.g. Mono 5.8 or .Net Core 3.1.10] (found under System -> Status)
- Browser and Version [e.g. chrome 86.0.4240.198] (Only needed for UI issues)
- Readarr Version [e.g. 0.3.0.430]
- Readarr Branch [e.g. master]
**Trace Logs**
Turn on Trace logs under Settings -> General and wait for the bug to occur again. **Upload the full log file here (or another site (e.g. pastebin) and link it). Issues will be closed, if they do not include this!**

View File

@@ -1,75 +0,0 @@
name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing open and closed 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
- **Readarr**: Readarr 0.1.0.432
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
- **Database**: Sqlite 3.36.0
value: |
- OS:
- Readarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
- Database:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Master
- Develop
- Nightly
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Trace Logs?
description: |
Trace Logs (https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files)
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Support via Discord
url: https://readarr.com/discord
url: https://discord.gg/rkEXY2Rbgn
about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/Readarr

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -1,38 +0,0 @@
name: Feature Request
description: 'Suggest an idea for Readarr'
labels: ['Type: Feature Request', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch.
options:
- label: I have searched the existing open and closed 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,16 +1,14 @@
#### Database Migration
YES - XXXX | NO
YES | NO
#### Description
A few sentences describing the overall goals of the pull request's commits.
#### Screenshot (if UI related)
#### Todos
- [ ] Tests
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
- [ ] [Wiki Updates](https://wiki.servarr.com)
- [ ] Wiki Updates
#### Issues Fixed or Closed by this PR
* Fixes #XXXX
*

View File

@@ -1,21 +0,0 @@
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: ''
issue-lock-labels: ''
issue-lock-comment: ''
issue-lock-reason: 'resolved'
process-only: ''

View File

@@ -1,21 +0,0 @@
name: 'Support requests'
on:
issues:
types: [labeled, unlabeled, reopened]
jobs:
support:
runs-on: ubuntu-latest
steps:
- uses: dessant/support-requests@v2
with:
github-token: ${{ github.token }}
support-label: 'Type: Support'
issue-comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord)
or [Subreddit](https://reddit.com/r/readarr)
close-issue: true
lock-issue: false

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"files.associations": {
"*.yaml": "home-assistant"
}
}

View File

@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@readarr.com>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,13 +1,50 @@
# How to Contribute
# How to Contribute #
We're always looking for people to help make Readarr even better, there are a number of ways to contribute.
This file has been moved to the wiki for the latest details please see the [contributing wiki page](https://wiki.servarr.com/readarr/contributing).
## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/Readarr) the better.
## Documentation
## Development ##
Setup guides, [FAQ](https://wiki.servarr.com/readarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better.
### 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/)
- .NET Core 3.1.
## Development
### Getting started ###
See the [Wiki Page](https://wiki.servarr.com/readarr/contributing)
1. Fork Readarr
2. Clone the repository into your development machine. [*info*](https://help.github.com/articles/working-with-repositories)
3. Grab the submodules `git submodule init && git submodule update`
4. Install the required Node Packages `yarn install`
5. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
6. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `netcoreapp31`
7. Debug the project in Visual Studio
8. Open http://localhost:8787
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Readarr/Readarr/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 Readarr'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 discord 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 2019 and WebStorm (to my knowledge)
### 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
- 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)
- new-feature (Good)
- fix-bug (Good)
- patch (Bad)
- develop (Bad)
If you have any questions about any of this, please let us know.

View File

@@ -1,77 +1,53 @@
# Readarr
[![Build Status](https://dev.azure.com/Readarr/Readarr/_apis/build/status/Readarr.Readarr?branchName=develop)](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/readarr/svg-badge.svg)](https://translate.servarr.com/engage/readarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/readarr)](https://wiki.servarr.com/readarr/installation#docker)
[![Donors on Open Collective](https://opencollective.com/Readarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Readarr/sponsors/badge.svg)](#sponsors)
[![Mega Sponsors on Open Collective](https://opencollective.com/Readarr/megasponsors/badge.svg)](#mega-sponsors)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/readarr)](https://hub.docker.com/r/hotio/readarr)
![Github Downloads](https://img.shields.io/github/downloads/readarr/readarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/readarr/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/readarr/sponsors/badge.svg)](#sponsors)
### Readarr is currently in beta testing and is generally still in a work in progress. Features may be broken, incomplete, or cause spontaneous combustion
### Readarr is in early stages of development, alpha/beta binary builds are not yet available. Use of any test builds isn't recommend, and may have detrimental effects on your library.
Readarr is an ebook and audiobook collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort, and rename them.
Note that only one type of a given book is supported. If you want both an audiobook and ebook of a given book you will need multiple instances.
Readarr is a ebook (and maybe eventually magazine/audiobook) collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new books from your favorite authors and will grab, sort and rename them.
## Major Features Include
## Major Features Include:
* Can watch for better quality of the ebooks and audiobooks you have and do an automatic upgrade. *e.g. from PDF to AZW3*
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically detects new books
* Can scan your existing library and download any missing books
* Automatically detects new books.
* Can scan your existing library and download any missing books.
* Automatic failed download handling will try another release if one fails
* Manual search so you can pick any release or to see why a release was not downloaded automatically
* Advanced customization for profiles, such that Readarr will always download the copy you want
* Fully configurable book renaming
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
* Full integration with Calibre (add to library, conversion) (Requires Calibre Content Server)
* Full integration with SABnzbd and NZBGet
* Full integration with Calibre (add to library, conversion)
* And a beautiful UI
## Support
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/readarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://readarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/readarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://discord.gg/rkEXY2Rbgn)
[![GitHub](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Readarr/Readarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/Readarr)
Note: GitHub Issues are for Bugs and Feature Requests Only
## Contributors
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Readarr/Readarr/issues)
## Contributors & Developers
[API Documentation](https://readarr.com/docs/api/)
This project exists thanks to all the people who contribute.
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/readarr/contributing)
[![Contributors List](https://opencollective.com/Readarr/contributors.svg?width=890&button=false)](https://github.com/Readarr/Readarr/graphs/contributors)
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Readarr/Readarr/graphs/contributors"><img src="https://opencollective.com/Readarr/contributors.svg?width=890&button=false" /></a>
## Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Readarr#backer)
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/readarr#backer)
[![Backers List](https://opencollective.com/Readarr/backers.svg?width=890)](https://opencollective.com/Readarr#backer)
<img src="https://opencollective.com/readarr/backers.svg?width=890"></a>
## Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/readarr#sponsor)
[![Sponsors List](https://opencollective.com/Readarr/sponsors.svg?width=890)](https://opencollective.com/readarr#sponsor)
<img src="https://opencollective.com/readarr/sponsors.svg?width=890"></a>
## Mega Sponsors
[![Mega Sponsors List](https://opencollective.com/Readarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/readarr#mega-sponsor)
## DigitalOcean
This project is also supported by DigitalOcean
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>
<img src="https://opencollective.com/readarr/tiers/mega-sponsor.svg?width=890"></a>
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022
* Copyright 2010-2021

View File

@@ -1,8 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities on Discord (preferred) to
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
us within 72 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity/severity.

File diff suppressed because it is too large Load Diff

168
build.sh
View File

@@ -1,4 +1,4 @@
#! /usr/bin/env bash
#! /bin/bash
set -e
outputFolder='_output'
@@ -27,25 +27,6 @@ UpdateVersionNumber()
fi
}
EnableExtraPlatformsInSDK()
{
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
fi
}
LintUI()
{
ProgressStart 'ESLint'
@@ -76,6 +57,9 @@ Build()
platform=Posix
fi
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
dotnet msbuild -restore $slnFile -p:Configuration=Release -p:Platform=$platform -t:PublishAllRids
@@ -93,11 +77,11 @@ YarnInstall()
ProgressEnd 'yarn install'
}
RunWebpack()
RunGulp()
{
ProgressStart 'Running webpack'
yarn run build --env production
ProgressEnd 'Running webpack'
ProgressStart 'Running gulp'
yarn run build --production
ProgressEnd 'Running gulp'
}
PackageFiles()
@@ -136,7 +120,7 @@ PackageLinux()
echo "Adding Readarr.Mono to UpdatePackage"
cp $folder/Readarr.Mono.* $folder/Readarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "netcoreapp3.1" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
fi
@@ -147,13 +131,17 @@ PackageLinux()
PackageMacOS()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating MacOS Package for $framework $runtime"
ProgressStart "Creating MacOS Package for $framework"
local folder=$artifactsFolder/$runtime/$framework/Readarr
local folder=$artifactsFolder/macos/$framework/Readarr
PackageFiles "$folder" "$framework" "$runtime"
PackageFiles "$folder" "$framework" "osx-x64"
if [ "$framework" = "net462" ]; then
echo "Adding Startup script"
cp macOS/Readarr $folder
fi
echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.*
@@ -164,7 +152,7 @@ PackageMacOS()
echo "Adding Readarr.Mono to UpdatePackage"
cp $folder/Readarr.Mono.* $folder/Readarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "netcoreapp3.1" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
fi
@@ -175,11 +163,10 @@ PackageMacOS()
PackageMacOSApp()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating macOS App Package for $framework $runtime"
ProgressStart "Creating macOS App Package for $framework"
local folder="$artifactsFolder/$runtime-app/$framework"
local folder=$artifactsFolder/macos-app/$framework
rm -rf $folder
mkdir -p $folder
@@ -187,7 +174,7 @@ PackageMacOSApp()
mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries"
cp -r $artifactsFolder/$runtime/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
cp -r $artifactsFolder/macos/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
echo "Removing Update Folder"
rm -r $folder/Readarr.app/Contents/MacOS/Readarr.Update
@@ -199,13 +186,12 @@ PackageWindows()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating $runtime Package for $framework"
ProgressStart "Creating Windows Package for $framework"
local folder=$artifactsFolder/$runtime/$framework/Readarr
PackageFiles "$folder" "$framework" "$runtime"
cp -r $outputFolder/$framework-windows/$runtime/publish/* $folder
echo "Removing Readarr.Mono"
rm -f $folder/Readarr.Mono.*
@@ -215,7 +201,7 @@ PackageWindows()
echo "Adding Readarr.Windows to UpdatePackage"
cp $folder/Readarr.Windows.* $folder/Readarr.Update
ProgressEnd "Creating $runtime Package for $framework"
ProgressEnd 'Creating Windows Package'
}
Package()
@@ -227,45 +213,19 @@ Package()
IFS='-' read -ra SPLIT <<< "$runtime"
case "${SPLIT[0]}" in
linux|freebsd*)
linux)
PackageLinux "$framework" "$runtime"
;;
win)
PackageWindows "$framework" "$runtime"
;;
osx)
PackageMacOS "$framework" "$runtime"
PackageMacOSApp "$framework" "$runtime"
PackageMacOS "$framework"
PackageMacOSApp "$framework"
;;
esac
}
BuildInstaller()
{
local framework="$1"
local runtime="$2"
./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
}
InstallInno()
{
ProgressStart "Installing portable Inno Setup"
rm -rf _inno
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
mkdir _inno
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
rm innosetup.exe
ProgressEnd "Installed portable Inno Setup"
}
RemoveInno()
{
rm -rf _inno
}
PackageTests()
{
local framework="$1"
@@ -297,10 +257,7 @@ if [ $# -eq 0 ]; then
BACKEND=YES
FRONTEND=YES
PACKAGES=YES
INSTALLER=NO
LINT=YES
ENABLE_EXTRA_PLATFORMS=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi
while [[ $# -gt 0 ]]
@@ -312,14 +269,6 @@ case $key in
BACKEND=YES
shift # past argument
;;
--enable-bsd|--enable-extra-platforms)
ENABLE_EXTRA_PLATFORMS=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument
;;
-r|--runtime)
RID="$2"
shift # past argument
@@ -338,10 +287,6 @@ case $key in
PACKAGES=YES
shift # past argument
;;
--installer)
INSTALLER=YES
shift # past argument
;;
--lint)
LINT=YES
shift # past argument
@@ -361,31 +306,18 @@ esac
done
set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ];
then
UpdateVersionNumber
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
EnableExtraPlatforms
fi
Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
PackageTests "net6.0" "win-x64"
PackageTests "net6.0" "win-x86"
PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi
PackageTests "netcoreapp3.1" "win-x64"
PackageTests "netcoreapp3.1" "win-x86"
PackageTests "netcoreapp3.1" "linux-x64"
PackageTests "netcoreapp3.1" "linux-musl-x64"
PackageTests "netcoreapp3.1" "osx-x64"
PackageTests "net462" "linux-x64"
else
PackageTests "$FRAMEWORK" "$RID"
fi
@@ -394,7 +326,7 @@ fi
if [ "$FRONTEND" = "YES" ];
then
YarnInstall
RunWebpack
RunGulp
fi
if [ "$LINT" = "YES" ];
@@ -413,30 +345,16 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then
Package "net6.0" "win-x64"
Package "net6.0" "win-x86"
Package "net6.0" "linux-x64"
Package "net6.0" "linux-musl-x64"
Package "net6.0" "linux-arm64"
Package "net6.0" "linux-musl-arm64"
Package "net6.0" "linux-arm"
Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then
Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi
Package "netcoreapp3.1" "win-x64"
Package "netcoreapp3.1" "win-x86"
Package "netcoreapp3.1" "linux-x64"
Package "netcoreapp3.1" "linux-musl-x64"
Package "netcoreapp3.1" "linux-arm64"
Package "netcoreapp3.1" "linux-musl-arm64"
Package "netcoreapp3.1" "linux-arm"
Package "netcoreapp3.1" "osx-x64"
Package "net462" "linux-x64"
else
Package "$FRAMEWORK" "$RID"
fi
fi
if [ "$INSTALLER" = "YES" ];
then
InstallInno
BuildInstaller "net6.0" "win-x64"
BuildInstaller "net6.0" "win-x86"
RemoveInno
fi

5
debian/changelog vendored Normal file
View File

@@ -0,0 +1,5 @@
nzbdrone {version} {branch}; urgency=low
* Automatic Release.
-- NzbDrone <contact@nzbdrone.com> Mon, 26 Aug 2013 00:00:00 -0700

1
debian/compat vendored Normal file
View File

@@ -0,0 +1 @@
8

12
debian/control vendored Normal file
View File

@@ -0,0 +1,12 @@
Section: web
Priority: optional
Maintainer: Sonarr <contact@nzbdrone.com>
Source: nzbdrone
Homepage: https://readarr.com
Vcs-Git: git@github.com:readarr/Readarr.git
Vcs-Browser: https://github.com/readarr/Readarr
Package: nzbdrone
Architecture: all
Depends: libmono-cil-dev (>= 3.2), sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
Description: Readarr is a music collection manager

24
debian/copyright vendored Executable file
View File

@@ -0,0 +1,24 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: nzbdrone
Source: https://github.com/readarr/Readarr
Files: *
Copyright: 2010-2016 Readarr <hello@readarr.com>
License: GPL-3.0+
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
.
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".

1
debian/install vendored Executable file
View File

@@ -0,0 +1 @@
nzbdrone_bin/* opt/NzbDrone

13
debian/rules vendored Executable file
View File

@@ -0,0 +1,13 @@
#!/usr/bin/make -f
# -*- makefile -*-
# Sample debian/rules that uses debhelper.
# This file was originally written by Joey Hess and Craig Small.
# As a special exception, when this file is copied by dh-make into a
# dh-make output file, you may use that output file without restriction.
# This special exception was added by Craig Small in version 0.37 of dh-make.
# Uncomment this to turn on verbose mode.
#export DH_VERBOSE=1
%:
dh $@

View File

@@ -6,10 +6,8 @@ const dirs = fs
.map((dirent) => dirent.name)
.join('|');
const frontendFolder = __dirname;
module.exports = {
parser: '@babel/eslint-parser',
parser: 'babel-eslint',
env: {
browser: true,
@@ -27,9 +25,6 @@ module.exports = {
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
babelOptions: {
configFile: `${frontendFolder}/babel.config.js`,
},
ecmaFeatures: {
modules: true,
impliedStrict: true
@@ -39,7 +34,6 @@ module.exports = {
plugins: [
'filenames',
'react',
'react-hooks',
'simple-import-sort',
'import'
],
@@ -277,7 +271,7 @@ module.exports = {
// ImportSort
'simple-import-sort/imports': 'error',
'simple-import-sort/sort': 'error',
'import/newline-after-import': 'error',
// React
@@ -309,15 +303,13 @@ module.exports = {
'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2,
'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2,
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error'
'react/jsx-wrap-multilines': 2
},
overrides: [
{
files: ['*.js'],
rules: {
'simple-import-sort/imports': [
'simple-import-sort/sort': [
'error',
{
groups: [

View File

@@ -1,271 +0,0 @@
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',
'react-middle-truncate': 'react-middle-truncate/lib/react-middle-truncate'
},
fallback: {
buffer: false,
http: false,
https: false,
url: false,
util: false,
net: false
}
},
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;
};

18
frontend/gulp/build.js Normal file
View File

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

8
frontend/gulp/clean.js Normal file
View File

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

42
frontend/gulp/copy.js Normal file
View File

@@ -0,0 +1,42 @@
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());
});
gulp.task('copyRobots', () => {
return gulp.src(paths.src.robots, { base: paths.src.root })
.pipe(cache('copyRobots'))
.pipe(print())
.pipe(gulp.dest(paths.dest.root))
.pipe(livereload());
});

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
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/`,
robots: `${root}/Content/robots.txt`,
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;

19
frontend/gulp/watch.js Normal file
View File

@@ -0,0 +1,19 @@
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'));
gulpWatch(paths.src.robots, gulp.series('copyRobots'));
}
gulp.task('watch', gulp.series('build', watch));

271
frontend/gulp/webpack.js Normal file
View File

@@ -0,0 +1,271 @@
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 OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
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 OptimizeCssAssetsPlugin({}),
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, webpack)
.on('error', errorHandler)
.pipe(gulp.dest('_output/UI'))
.on('error', errorHandler)
.pipe(livereload())
.on('error', errorHandler);
});

View File

@@ -1,31 +1,23 @@
const reload = require('require-nocache')(module);
const cssVarsFiles = [
'./src/Styles/Variables/dimensions',
'./src/Styles/Variables/fonts',
'./src/Styles/Variables/animations',
'./src/Styles/Variables/zIndexes'
].map(require.resolve);
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 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'
]
return config;
};

View File

@@ -14,14 +14,13 @@ import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
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 BlocklistRowConnector from './BlocklistRowConnector';
import BlacklistRowConnector from './BlacklistRowConnector';
class Blocklist extends Component {
class Blacklist extends Component {
//
// Lifecycle
@@ -61,33 +60,33 @@ class Blocklist extends Component {
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
@@ -103,8 +102,8 @@ class Blocklist extends Component {
columns,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
@@ -121,11 +120,11 @@ class Blocklist extends Component {
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blocklist')}>
<PageContent title="Blacklist">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
@@ -133,10 +132,10 @@ class Blocklist extends Component {
/>
<PageToolbarButton
label={translate('Clear')}
label="Clear"
iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
@@ -146,7 +145,7 @@ class Blocklist extends Component {
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
@@ -161,15 +160,13 @@ class Blocklist extends Component {
{
!isAnyFetching && !!error &&
<div>
{translate('UnableToLoadBlocklist')}
</div>
<div>Unable to load blacklist</div>
}
{
isAllPopulated && !error && !items.length &&
<div>
{translate('NoHistoryBlocklist')}
No history blacklist
</div>
}
@@ -188,7 +185,7 @@ class Blocklist extends Component {
{
items.map((item) => {
return (
<BlocklistRowConnector
<BlacklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
@@ -213,9 +210,9 @@ class Blocklist extends Component {
<ConfirmModal
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('RemoveSelectedMessageText')}
confirmLabel={translate('RemoveSelected')}
title="Remove Selected"
message={'Are you sure you want to remove the selected items from the blacklist?'}
confirmLabel="Remove Selected"
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
/>
@@ -224,7 +221,7 @@ class Blocklist extends Component {
}
}
Blocklist.propTypes = {
Blacklist.propTypes = {
isAuthorFetching: PropTypes.bool.isRequired,
isAuthorPopulated: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
@@ -234,9 +231,9 @@ Blocklist.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
onClearBlacklistPress: PropTypes.func.isRequired
};
export default Blocklist;
export default Blacklist;

View File

@@ -4,34 +4,34 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import * as blocklistActions from 'Store/Actions/blocklistActions';
import * as blacklistActions from 'Store/Actions/blacklistActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist';
import Blacklist from './Blacklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blocklist,
(state) => state.blacklist,
(state) => state.authors,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
(blocklist, authors, isClearingBlocklistExecuting) => {
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, authors, isClearingBlacklistExecuting) => {
return {
isAuthorFetching: authors.isFetching,
isAuthorPopulated: authors.isPopulated,
isClearingBlocklistExecuting,
...blocklist
isClearingBlacklistExecuting,
...blacklist
};
}
);
}
const mapDispatchToProps = {
...blocklistActions,
...blacklistActions,
executeCommand
};
class BlocklistConnector extends Component {
class BlacklistConnector extends Component {
//
// Lifecycle
@@ -39,27 +39,27 @@ class BlocklistConnector extends Component {
componentDidMount() {
const {
useCurrentPage,
fetchBlocklist,
gotoBlocklistFirstPage
fetchBlacklist,
gotoBlacklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlocklist();
fetchBlacklist();
} else {
gotoBlocklistFirstPage();
gotoBlacklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
this.props.gotoBlocklistFirstPage();
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlocklist();
this.props.clearBlacklist();
unregisterPagePopulator(this.repopulate);
}
@@ -67,57 +67,57 @@ class BlocklistConnector extends Component {
// Control
repopulate = () => {
this.props.fetchBlocklist();
};
this.props.fetchBlacklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage();
};
this.props.gotoBlacklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage();
};
this.props.gotoBlacklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlocklistNextPage();
};
this.props.gotoBlacklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlocklistLastPage();
};
this.props.gotoBlacklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page });
};
this.props.gotoBlacklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids });
};
this.props.removeBlacklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey });
};
this.props.setBlacklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload);
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlocklistFirstPage();
this.props.gotoBlacklistFirstPage();
}
};
}
onClearBlocklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
};
onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
}
//
// Render
render() {
return (
<Blocklist
<Blacklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
@@ -126,30 +126,30 @@ class BlocklistConnector extends Component {
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress}
onClearBlacklistPress={this.onClearBlacklistPress}
{...this.props}
/>
);
}
}
BlocklistConnector.propTypes = {
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: 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,
fetchBlacklist: PropTypes.func.isRequired,
gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
removeBlacklistItems: 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)(BlocklistConnector)
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
);

View File

@@ -8,9 +8,8 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate';
class BlocklistDetailsModal extends Component {
class BlacklistDetailsModal extends Component {
//
// Render
@@ -40,19 +39,19 @@ class BlocklistDetailsModal extends Component {
<ModalBody>
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Protocol')}
title="Protocol"
data={protocol}
/>
{
!!message &&
<DescriptionListItem
title={translate('Indexer')}
title="Indexer"
data={indexer}
/>
}
@@ -60,7 +59,7 @@ class BlocklistDetailsModal extends Component {
{
!!message &&
<DescriptionListItem
title={translate('Message')}
title="Message"
data={message}
/>
}
@@ -78,7 +77,7 @@ class BlocklistDetailsModal extends Component {
}
}
BlocklistDetailsModal.propTypes = {
BlacklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
@@ -87,4 +86,4 @@ BlocklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired
};
export default BlocklistDetailsModal;
export default BlacklistDetailsModal;

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
@@ -9,11 +8,10 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import BlocklistDetailsModal from './BlocklistDetailsModal';
import styles from './BlocklistRow.css';
import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlacklistRow.css';
class BlocklistRow extends Component {
class BlacklistRow extends Component {
//
// Lifecycle
@@ -31,11 +29,11 @@ class BlocklistRow extends Component {
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
}
//
// Render
@@ -46,7 +44,6 @@ class BlocklistRow extends Component {
author,
sourceTitle,
quality,
customFormats,
date,
protocol,
indexer,
@@ -80,7 +77,7 @@ class BlocklistRow extends Component {
return null;
}
if (name === 'authorMetadata.sortName') {
if (name === 'authors.sortName') {
return (
<TableRowCell key={name}>
<AuthorNameLink
@@ -112,16 +109,6 @@ class BlocklistRow extends Component {
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<BookFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCellConnector
@@ -154,7 +141,7 @@ class BlocklistRow extends Component {
/>
<IconButton
title={translate('RemoveFromBlocklist')}
title="Remove from blacklist"
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
@@ -167,7 +154,7 @@ class BlocklistRow extends Component {
})
}
<BlocklistDetailsModal
<BlacklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
@@ -181,12 +168,11 @@ class BlocklistRow extends Component {
}
BlocklistRow.propTypes = {
BlacklistRow.propTypes = {
id: PropTypes.number.isRequired,
author: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
date: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
@@ -197,4 +183,4 @@ BlocklistRow.propTypes = {
onRemovePress: PropTypes.func.isRequired
};
export default BlocklistRow;
export default BlacklistRow;

View File

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

View File

@@ -9,8 +9,6 @@ import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import formatDateTime from 'Utilities/Date/formatDateTime';
import formatAge from 'Utilities/Number/formatAge';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import translate from 'Utilities/String/translate';
import styles from './HistoryDetails.css';
function getDetailedList(statusMessages) {
@@ -66,10 +64,8 @@ function HistoryDetails(props) {
const {
indexer,
releaseGroup,
customFormatScore,
nzbInfoUrl,
downloadClient,
downloadClientName,
downloadId,
age,
ageHours,
@@ -77,20 +73,18 @@ function HistoryDetails(props) {
publishedDate
} = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
{
!!indexer &&
<DescriptionListItem
title={translate('Indexer')}
title="Indexer"
data={indexer}
/>
}
@@ -99,22 +93,13 @@ function HistoryDetails(props) {
!!releaseGroup &&
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ReleaseGroup')}
title="Release Group"
data={releaseGroup}
/>
}
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title="Custom Format Score"
data={formatPreferredWordScore(customFormatScore)}
/> :
null
}
{
nzbInfoUrl ?
!!nzbInfoUrl &&
<span>
<DescriptionListItemTitle>
Info URL
@@ -123,23 +108,21 @@ function HistoryDetails(props) {
<DescriptionListItemDescription>
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
</DescriptionListItemDescription>
</span> :
null
</span>
}
{
downloadClientNameInfo ?
!!downloadClient &&
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClientNameInfo}
/> :
null
title="Download Client"
data={downloadClient}
/>
}
{
!!downloadId &&
<DescriptionListItem
title={translate('GrabID')}
title="Grab ID"
data={downloadId}
/>
}
@@ -147,7 +130,7 @@ function HistoryDetails(props) {
{
!!indexer &&
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)}
/>
}
@@ -155,7 +138,7 @@ function HistoryDetails(props) {
{
!!publishedDate &&
<DescriptionListItem
title={translate('PublishedDate')}
title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/>
}
@@ -172,14 +155,14 @@ function HistoryDetails(props) {
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
{
!!message &&
<DescriptionListItem
title={translate('Message')}
title="Message"
data={message}
/>
}
@@ -189,7 +172,6 @@ function HistoryDetails(props) {
if (eventType === 'bookFileImported') {
const {
customFormatScore,
droppedPath,
importedPath
} = data;
@@ -198,7 +180,7 @@ function HistoryDetails(props) {
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
@@ -206,28 +188,18 @@ function HistoryDetails(props) {
!!droppedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Source')}
title="Source"
data={droppedPath}
/>
}
{
importedPath ?
!!importedPath &&
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('ImportedTo')}
title="Imported To"
data={importedPath}
/> :
null
}
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title="Custom Format Score"
data={formatPreferredWordScore(customFormatScore)}
/> :
null
/>
}
</DescriptionList>
);
@@ -235,21 +207,20 @@ function HistoryDetails(props) {
if (eventType === 'bookFileDeleted') {
const {
reason,
customFormatScore
reason
} = data;
let reasonMessage = '';
switch (reason) {
case 'Manual':
reasonMessage = translate('FileWasDeletedByViaUI');
reasonMessage = 'File was deleted by via UI';
break;
case 'MissingFromDisk':
reasonMessage = translate('MissingFromDisk');
reasonMessage = 'Readarr was unable to find the file on disk so it was removed';
break;
case 'Upgrade':
reasonMessage = translate('FileWasDeletedByUpgrade');
reasonMessage = 'File was deleted to import an upgrade';
break;
default:
reasonMessage = '';
@@ -258,23 +229,14 @@ function HistoryDetails(props) {
return (
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
<DescriptionListItem
title={translate('Reason')}
title="Reason"
data={reasonMessage}
/>
{
customFormatScore && customFormatScore !== '0' ?
<DescriptionListItem
title="Custom Format Score"
data={formatPreferredWordScore(customFormatScore)}
/> :
null
}
</DescriptionList>
);
}
@@ -288,12 +250,12 @@ function HistoryDetails(props) {
return (
<DescriptionList>
<DescriptionListItem
title={translate('SourcePath')}
title="Source Path"
data={sourcePath}
/>
<DescriptionListItem
title={translate('DestinationPath')}
title="Destination Path"
data={path}
/>
</DescriptionList>
@@ -309,7 +271,7 @@ function HistoryDetails(props) {
return (
<DescriptionList>
<DescriptionListItem
title={translate('Path')}
title="Path"
data={sourceTitle}
/>
{
@@ -324,7 +286,7 @@ function HistoryDetails(props) {
})
}
<DescriptionListItem
title={translate('ExistingTagsScrubbed')}
title="Existing tags scrubbed"
data={tagsScrubbed === 'True' ? <Icon name={icons.CHECK} /> : <Icon name={icons.REMOVE} />}
/>
</DescriptionList>
@@ -339,14 +301,14 @@ function HistoryDetails(props) {
return (
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
{
!!statusMessages &&
<DescriptionListItem
title={translate('ImportFailures')}
title="Import failures"
data={getDetailedList(JSON.parse(statusMessages))}
/>
}
@@ -370,14 +332,14 @@ function HistoryDetails(props) {
return (
<DescriptionList>
<DescriptionListItem
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
{
!!indexer &&
<DescriptionListItem
title={translate('Indexer')}
title="Indexer"
data={indexer}
/>
}
@@ -385,7 +347,7 @@ function HistoryDetails(props) {
{
!!releaseGroup &&
<DescriptionListItem
title={translate('ReleaseGroup')}
title="Release Group"
data={releaseGroup}
/>
}
@@ -406,7 +368,7 @@ function HistoryDetails(props) {
{
!!downloadClient &&
<DescriptionListItem
title={translate('DownloadClient')}
title="Download Client"
data={downloadClient}
/>
}
@@ -414,7 +376,7 @@ function HistoryDetails(props) {
{
!!downloadId &&
<DescriptionListItem
title={translate('GrabID')}
title="Grab ID"
data={downloadId}
/>
}
@@ -422,7 +384,7 @@ function HistoryDetails(props) {
{
!!indexer &&
<DescriptionListItem
title={translate('AgeWhenGrabbed')}
title="Age (when grabbed)"
data={formatAge(age, ageHours, ageMinutes)}
/>
}
@@ -430,7 +392,7 @@ function HistoryDetails(props) {
{
!!publishedDate &&
<DescriptionListItem
title={translate('PublishedDate')}
title="Published Date"
data={formatDateTime(publishedDate, shortDateFormat, timeFormat, { includeSeconds: true })}
/>
}
@@ -447,14 +409,14 @@ function HistoryDetails(props) {
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
{
!!message &&
<DescriptionListItem
title={translate('Message')}
title="Message"
data={message}
/>
}
@@ -466,7 +428,7 @@ function HistoryDetails(props) {
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Name')}
title="Name"
data={sourceTitle}
/>
</DescriptionList>

View File

@@ -12,11 +12,32 @@ import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
//
// Lifecycle
shouldComponentUpdate(nextProps) {
// Don't update when fetching has completed if items have changed,
// before books start fetching or when books start fetching.
if (
(
this.props.isFetching &&
nextProps.isPopulated &&
hasDifferentItems(this.props.items, nextProps.items)
) ||
(!this.props.isBooksFetching && nextProps.isBooksFetching)
) {
return false;
}
return true;
}
//
// Render
@@ -45,11 +66,11 @@ class History extends Component {
const hasError = error || booksError;
return (
<PageContent title={translate('History')}>
<PageContent title="History">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
label="Refresh"
iconName={icons.REFRESH}
isSpinning={isFetching}
onPress={onFirstPagePress}
@@ -62,7 +83,7 @@ class History extends Component {
columns={columns}
>
<PageToolbarButton
label={translate('Options')}
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
@@ -85,9 +106,7 @@ class History extends Component {
{
!isFetchingAny && hasError &&
<div>
{translate('UnableToLoadHistory')}
</div>
<div>Unable to load history</div>
}
{

View File

@@ -3,7 +3,10 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import withCurrentPage from 'Components/withCurrentPage';
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
import * as historyActions from 'Store/Actions/historyActions';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import History from './History';
@@ -26,7 +29,9 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
...historyActions
...historyActions,
fetchBooks,
clearBooks
};
class HistoryConnector extends Component {
@@ -50,9 +55,21 @@ class HistoryConnector extends Component {
}
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const bookIds = selectUniqueIds(this.props.items, 'bookId');
if (bookIds.length) {
this.props.fetchBooks({ bookIds });
} else {
this.props.clearBooks();
}
}
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearHistory();
this.props.clearBooks();
}
//
@@ -60,38 +77,38 @@ class HistoryConnector extends Component {
repopulate = () => {
this.props.fetchHistory();
};
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoHistoryFirstPage();
};
}
onPreviousPagePress = () => {
this.props.gotoHistoryPreviousPage();
};
}
onNextPagePress = () => {
this.props.gotoHistoryNextPage();
};
}
onLastPagePress = () => {
this.props.gotoHistoryLastPage();
};
}
onPageSelect = (page) => {
this.props.gotoHistoryPage({ page });
};
}
onSortPress = (sortKey) => {
this.props.setHistorySort({ sortKey });
};
}
onFilterSelect = (selectedFilterKey) => {
this.props.setHistoryFilter({ selectedFilterKey });
};
}
onTableOptionChange = (payload) => {
this.props.setHistoryTableOption(payload);
@@ -99,7 +116,7 @@ class HistoryConnector extends Component {
if (payload.pageSize) {
this.props.gotoHistoryFirstPage();
}
};
}
//
// Render
@@ -133,7 +150,9 @@ HistoryConnector.propTypes = {
setHistorySort: PropTypes.func.isRequired,
setHistoryFilter: PropTypes.func.isRequired,
setHistoryTableOption: PropTypes.func.isRequired,
clearHistory: PropTypes.func.isRequired
clearHistory: PropTypes.func.isRequired,
fetchBooks: PropTypes.func.isRequired,
clearBooks: PropTypes.func.isRequired
};
export default withCurrentPage(

View File

@@ -23,6 +23,8 @@ function getIconName(eventType) {
return icons.RETAG;
case 'bookImportIncomplete':
return icons.DOWNLOADED;
case 'downloadImported':
return icons.DOWNLOADED;
case 'downloadIgnored':
return icons.IGNORE;
default:
@@ -59,6 +61,8 @@ function getTooltip(eventType, data) {
return 'Book file tags updated';
case 'bookImportIncomplete':
return 'Files downloaded but not all could be imported';
case 'downloadImported':
return 'Download completed and successfully imported';
case 'downloadIgnored':
return 'Book Download Ignored';
default:

View File

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

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import BookTitleLink from 'Book/BookTitleLink';
import IconButton from 'Components/Link/IconButton';
@@ -9,7 +8,6 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css';
@@ -42,11 +40,11 @@ class HistoryRow extends Component {
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
}
//
// Render
@@ -56,7 +54,6 @@ class HistoryRow extends Component {
author,
book,
quality,
customFormats,
qualityCutoffNotMet,
eventType,
sourceTitle,
@@ -96,7 +93,7 @@ class HistoryRow extends Component {
);
}
if (name === 'authorMetadata.sortName') {
if (name === 'authors.sortName') {
return (
<TableRowCell key={name}>
<AuthorNameLink
@@ -130,16 +127,6 @@ class HistoryRow extends Component {
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<BookFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'date') {
return (
<RelativeDateCellConnector
@@ -171,17 +158,6 @@ class HistoryRow extends Component {
);
}
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
{formatPreferredWordScore(data.customFormatScore)}
</TableRowCell>
);
}
if (name === 'releaseGroup') {
return (
<TableRowCell
@@ -193,16 +169,6 @@ class HistoryRow extends Component {
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell
@@ -243,7 +209,6 @@ HistoryRow.propTypes = {
author: PropTypes.object.isRequired,
book: PropTypes.object,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
qualityCutoffNotMet: PropTypes.bool.isRequired,
eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired,

View File

@@ -49,7 +49,7 @@ class HistoryRowConnector extends Component {
onMarkAsFailedPress = () => {
this.props.markAsFailed({ id: this.props.id });
};
}
//
// Render

View File

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

View File

@@ -15,7 +15,6 @@ import TablePager from 'Components/Table/TablePager';
import { align, icons } from 'Helpers/Props';
import getRemovedItems from 'Utilities/Object/getRemovedItems';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll';
@@ -32,8 +31,6 @@ class Queue extends Component {
constructor(props, context) {
super(props, context);
this._shouldBlockRefresh = false;
this.state = {
allSelected: false,
allUnselected: false,
@@ -45,14 +42,6 @@ class Queue extends Component {
};
}
shouldComponentUpdate() {
if (this._shouldBlockRefresh) {
return false;
}
return true;
}
componentDidUpdate(prevProps) {
const {
items,
@@ -75,23 +64,13 @@ class Queue extends Component {
return;
}
const nextState = {};
if (prevProps.items !== items) {
nextState.items = items;
}
const selectedIds = this.getSelectedIds();
const isPendingSelected = _.some(this.props.items, (item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
});
if (isPendingSelected !== this.state.isPendingSelected) {
nextState.isPendingSelected = isPendingSelected;
}
if (!_.isEmpty(nextState)) {
this.setState(nextState);
this.setState({ isPendingSelected });
}
}
@@ -100,45 +79,37 @@ class Queue extends Component {
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
}
//
// Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
};
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);
});
};
}
onGrabSelectedPress = () => {
this.props.onGrabSelectedPress(this.getSelectedIds());
};
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true }, () => {
this._shouldBlockRefresh = true;
});
};
this.setState({ isConfirmRemoveModalOpen: 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 });
};
}
//
// Render
@@ -148,6 +119,7 @@ class Queue extends Component {
isFetching,
isPopulated,
error,
items,
isAuthorFetching,
isAuthorPopulated,
isBooksFetching,
@@ -167,8 +139,7 @@ class Queue extends Component {
allUnselected,
selectedState,
isConfirmRemoveModalOpen,
isPendingSelected,
items
isPendingSelected
} = this.state;
const isRefreshing = isFetching || isAuthorFetching || isBooksFetching || isRefreshMonitoredDownloadsExecuting;
@@ -179,11 +150,11 @@ class Queue extends Component {
const disableSelectedActions = selectedCount === 0;
return (
<PageContent title={translate('Queue')}>
<PageContent title="Queue">
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('Refresh')}
label="Refresh"
iconName={icons.REFRESH}
isSpinning={isRefreshing}
onPress={onRefreshPress}
@@ -192,7 +163,7 @@ class Queue extends Component {
<PageToolbarSeparator />
<PageToolbarButton
label={translate('GrabSelected')}
label="Grab Selected"
iconName={icons.DOWNLOAD}
isDisabled={disableSelectedActions || !isPendingSelected}
isSpinning={isGrabbing}
@@ -200,7 +171,7 @@ class Queue extends Component {
/>
<PageToolbarButton
label={translate('RemoveSelected')}
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={disableSelectedActions}
isSpinning={isRemoving}
@@ -217,7 +188,7 @@ class Queue extends Component {
optionsComponent={QueueOptionsConnector}
>
<PageToolbarButton
label={translate('Options')}
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
@@ -226,29 +197,26 @@ class Queue extends Component {
<PageContentBody>
{
isRefreshing && !isAllPopulated ?
<LoadingIndicator /> :
null
isRefreshing && !isAllPopulated &&
<LoadingIndicator />
}
{
!isRefreshing && hasError ?
!isRefreshing && hasError &&
<div>
{translate('FailedToLoadQueue')}
</div> :
null
Failed to load Queue
</div>
}
{
isAllPopulated && !hasError && !items.length ?
isPopulated && !hasError && !items.length &&
<div>
{translate('QueueIsEmpty')}
</div> :
null
Queue is empty
</div>
}
{
isAllPopulated && !hasError && !!items.length ?
isAllPopulated && !hasError && !!items.length &&
<div>
<Table
columns={columns}
@@ -270,7 +238,6 @@ class Queue extends Component {
columns={columns}
{...item}
onSelectedChange={this.onSelectedChange}
onQueueRowModalOpenOrClose={this.onQueueRowModalOpenOrClose}
/>
);
})
@@ -283,8 +250,7 @@ class Queue extends Component {
isFetching={isRefreshing}
{...otherProps}
/>
</div> :
null
</div>
}
</PageContentBody>
@@ -298,17 +264,6 @@ class Queue extends Component {
return !!(item && item.authorId && item.bookId);
})
)}
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

@@ -4,9 +4,12 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage';
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as queueActions from 'Store/Actions/queueActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Queue from './Queue';
@@ -34,6 +37,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
...queueActions,
fetchBooks,
clearBooks,
executeCommand
};
@@ -46,7 +51,6 @@ class QueueConnector extends Component {
const {
useCurrentPage,
fetchQueue,
fetchQueueStatus,
gotoQueueFirstPage
} = this.props;
@@ -57,11 +61,19 @@ class QueueConnector extends Component {
} else {
gotoQueueFirstPage();
}
fetchQueueStatus();
}
componentDidUpdate(prevProps) {
if (hasDifferentItems(prevProps.items, this.props.items)) {
const bookIds = selectUniqueIds(this.props.items, 'bookId');
if (bookIds.length) {
this.props.fetchBooks({ bookIds });
} else {
this.props.clearBooks();
}
}
if (
this.props.includeUnknownAuthorItems !==
prevProps.includeUnknownAuthorItems
@@ -72,6 +84,8 @@ class QueueConnector extends Component {
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
this.props.clearQueue();
this.props.clearBooks();
}
//
@@ -79,34 +93,34 @@ class QueueConnector extends Component {
repopulate = () => {
this.props.fetchQueue();
};
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoQueueFirstPage();
};
}
onPreviousPagePress = () => {
this.props.gotoQueuePreviousPage();
};
}
onNextPagePress = () => {
this.props.gotoQueueNextPage();
};
}
onLastPagePress = () => {
this.props.gotoQueueLastPage();
};
}
onPageSelect = (page) => {
this.props.gotoQueuePage({ page });
};
}
onSortPress = (sortKey) => {
this.props.setQueueSort({ sortKey });
};
}
onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload);
@@ -114,21 +128,21 @@ class QueueConnector extends Component {
if (payload.pageSize) {
this.props.gotoQueueFirstPage();
}
};
}
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS
});
};
}
onGrabSelectedPress = (ids) => {
this.props.grabQueueItems({ ids });
};
}
onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload);
};
}
//
// Render
@@ -157,7 +171,6 @@ QueueConnector.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
includeUnknownAuthorItems: PropTypes.bool.isRequired,
fetchQueue: PropTypes.func.isRequired,
fetchQueueStatus: PropTypes.func.isRequired,
gotoQueueFirstPage: PropTypes.func.isRequired,
gotoQueuePreviousPage: PropTypes.func.isRequired,
gotoQueueNextPage: PropTypes.func.isRequired,
@@ -168,6 +181,8 @@ QueueConnector.propTypes = {
clearQueue: PropTypes.func.isRequired,
grabQueueItems: PropTypes.func.isRequired,
removeQueueItems: PropTypes.func.isRequired,
fetchBooks: PropTypes.func.isRequired,
clearBooks: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

View File

@@ -3,7 +3,6 @@ import PropTypes from 'prop-types';
import React from 'react';
import Icon from 'Components/Icon';
import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
function QueueDetails(props) {
const {
@@ -24,7 +23,7 @@ function QueueDetails(props) {
return (
<Icon
name={icons.PENDING}
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])}
title={`Release will be processed ${moment(estimatedCompletionTime).fromNow()}`}
/>
);
}
@@ -35,7 +34,7 @@ function QueueDetails(props) {
<Icon
name={icons.DOWNLOAD}
kind={kinds.DANGER}
title={translate('ImportFailedInterp', [errorMessage])}
title={`Import failed: ${errorMessage}`}
/>
);
}
@@ -48,7 +47,7 @@ function QueueDetails(props) {
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={translate('DownloadFailedInterp', [errorMessage])}
title={`Download failed: ${errorMessage}`}
/>
);
}
@@ -58,7 +57,7 @@ function QueueDetails(props) {
<Icon
name={icons.DOWNLOADING}
kind={kinds.DANGER}
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')}
title="Download failed: check download client for more details"
/>
);
}
@@ -68,7 +67,7 @@ function QueueDetails(props) {
<Icon
name={icons.DOWNLOADING}
kind={kinds.WARNING}
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')}
title="Download warning: check download client for more details"
/>
);
}
@@ -77,7 +76,7 @@ function QueueDetails(props) {
return (
<Icon
name={icons.DOWNLOADING}
title={translate('BookIsDownloadingInterp', [progress.toFixed(1), title])}
title={`Book is downloading - ${progress.toFixed(1)}% ${title}`}
/>
);
}

View File

@@ -4,7 +4,6 @@ import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class QueueOptions extends Component {
@@ -42,7 +41,7 @@ class QueueOptions extends Component {
[name]: value
});
});
};
}
//
// Render
@@ -55,15 +54,13 @@ class QueueOptions extends Component {
return (
<Fragment>
<FormGroup>
<FormLabel>
{translate('ShowUnknownAuthorItems')}
</FormLabel>
<FormLabel>Show Unknown Author Items</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="includeUnknownAuthorItems"
value={includeUnknownAuthorItems}
helpText={translate('IncludeUnknownAuthorItemsHelpText')}
helpText="Show items without a author in the queue, this could include removed authors, movies or anything else in Readarr's category"
onChange={this.onOptionChange}
/>
</FormGroup>

View File

@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import AuthorNameLink from 'Author/AuthorNameLink';
import BookFormats from 'Book/BookFormats';
import BookQuality from 'Book/BookQuality';
import BookTitleLink from 'Book/BookTitleLink';
import Icon from 'Components/Icon';
@@ -16,8 +15,6 @@ import TableRow from 'Components/Table/TableRow';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import QueueStatusCell from './QueueStatusCell';
import RemoveQueueItemModal from './RemoveQueueItemModal';
import TimeleftCell from './TimeleftCell';
@@ -42,37 +39,24 @@ class QueueRow extends Component {
onRemoveQueueItemPress = () => {
this.setState({ isRemoveQueueItemModalOpen: true });
};
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipredownload);
}
onRemoveQueueItemModalConfirmed = (blacklist, skipredownload) => {
this.props.onRemoveQueueItemPress(blacklist, skipredownload);
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 });
};
}
//
// Render
@@ -90,7 +74,6 @@ class QueueRow extends Component {
author,
book,
quality,
customFormats,
protocol,
indexer,
outputPath,
@@ -154,7 +137,7 @@ class QueueRow extends Component {
);
}
if (name === 'authorMetadata.sortName') {
if (name === 'authors.sortName') {
return (
<TableRowCell key={name}>
{
@@ -212,16 +195,6 @@ class QueueRow extends Component {
);
}
if (name === 'customFormats') {
return (
<TableRowCell key={name}>
<BookFormats
formats={customFormats}
/>
</TableRowCell>
);
}
if (name === 'protocol') {
return (
<TableRowCell key={name}>
@@ -256,12 +229,6 @@ class QueueRow extends Component {
);
}
if (name === 'size') {
return (
<TableRowCell key={name}>{formatBytes(size)}</TableRowCell>
);
}
if (name === 'outputPath') {
return (
<TableRowCell key={name}>
@@ -318,7 +285,7 @@ class QueueRow extends Component {
kind={kinds.DANGER}
/>
}
title={translate('ManualDownload')}
title="Manual Download"
body="This release failed parsing checks and was manually downloaded from an interactive search. Import is likely to fail."
position={tooltipPositions.LEFT}
/>
@@ -343,7 +310,7 @@ class QueueRow extends Component {
}
<SpinnerIconButton
title={translate('RemoveFromQueue')}
title="Remove from queue"
name={icons.REMOVE}
isSpinning={isRemoving}
onPress={this.onRemoveQueueItemPress}
@@ -368,8 +335,7 @@ class QueueRow extends Component {
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!author}
isPending={isPending}
canIgnore={!!(author && book)}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>
@@ -391,7 +357,6 @@ QueueRow.propTypes = {
author: PropTypes.object,
book: PropTypes.object,
quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
protocol: PropTypes.string.isRequired,
indexer: PropTypes.string,
outputPath: PropTypes.string,
@@ -411,8 +376,7 @@ QueueRow.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSelectedChange: PropTypes.func.isRequired,
onGrabPress: PropTypes.func.isRequired,
onRemoveQueueItemPress: PropTypes.func.isRequired,
onQueueRowModalOpenOrClose: PropTypes.func.isRequired
onRemoveQueueItemPress: PropTypes.func.isRequired
};
QueueRow.defaultProps = {

View File

@@ -41,11 +41,11 @@ class QueueRowConnector extends Component {
onGrabPress = () => {
this.props.grabQueueItem({ id: this.props.id });
};
}
onRemoveQueueItemPress = (payload) => {
this.props.removeQueueItem({ id: this.props.id, ...payload });
};
}
//
// Render

View File

@@ -4,7 +4,6 @@ import Icon from 'Components/Icon';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import Popover from 'Components/Tooltip/Popover';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './QueueStatusCell.css';
function getDetailedPopoverBody(statusMessages) {
@@ -50,7 +49,11 @@ function QueueStatusCell(props) {
// status === 'downloading'
let iconName = icons.DOWNLOADING;
let iconKind = kinds.DEFAULT;
let title = translate('Title');
let title = 'Downloading';
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'paused') {
iconName = icons.PAUSED;
@@ -68,24 +71,17 @@ function QueueStatusCell(props) {
if (trackedDownloadState === 'importPending') {
title += ' - Waiting to Import';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'importing') {
title += ' - Importing';
iconKind = kinds.PURPLE;
}
if (trackedDownloadState === 'failedPending') {
title += ' - Waiting to Process';
iconKind = kinds.DANGER;
}
}
if (hasWarning) {
iconKind = kinds.WARNING;
}
if (status === 'delay') {
iconName = icons.PENDING;
title = 'Pending';

View File

@@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class RemoveQueueItemModal extends Component {
@@ -22,7 +21,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blocklist: false,
blacklist: false,
skipredownload: false
};
}
@@ -33,37 +32,37 @@ class RemoveQueueItemModal extends Component {
resetState = function() {
this.setState({
remove: true,
blocklist: false,
blacklist: false,
skipredownload: false
});
};
}
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
}
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onSkipReDownloadChange = ({ value }) => {
this.setState({ skipredownload: value });
};
}
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
}
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
}
//
// Render
@@ -72,11 +71,10 @@ class RemoveQueueItemModal extends Component {
const {
isOpen,
sourceTitle,
canIgnore,
isPending
canIgnore
} = this.props;
const { remove, blocklist, skipredownload } = this.state;
const { remove, blacklist, skipredownload } = this.state;
return (
<Modal
@@ -96,50 +94,40 @@ class RemoveQueueItemModal extends Component {
Are you sure you want to remove '{sourceTitle}' from the queue?
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{translate('BlocklistRelease')}
</FormLabel>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
onChange={this.onBlocklistChange}
name="remove"
value={remove}
helpTextWarning="Removing will remove the download and the file(s) from the download client."
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>Blacklist Release</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText="Prevents Readarr from automatically grabbing this release again"
onChange={this.onBlacklistChange}
/>
</FormGroup>
{
blocklist &&
blacklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
</FormLabel>
<FormLabel>Skip Redownload</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipredownload"
value={skipredownload}
helpText={translate('SkipredownloadHelpText')}
helpText="Prevents Readarr from trying download an alternative release for this item"
onChange={this.onSkipReDownloadChange}
/>
</FormGroup>
@@ -169,7 +157,6 @@ 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

@@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemsModal.css';
class RemoveQueueItemsModal extends Component {
@@ -23,7 +22,7 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blocklist: false,
blacklist: false,
skipredownload: false
};
}
@@ -31,40 +30,40 @@ class RemoveQueueItemsModal extends Component {
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipredownload: false
});
};
resetState = function() {
this.setState({
remove: true,
blacklist: false,
skipredownload: false
});
}
//
// Listeners
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
}
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onSkipReDownloadChange = ({ value }) => {
this.setState({ skipredownload: value });
};
}
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
}
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
}
//
// Render
@@ -73,11 +72,10 @@ class RemoveQueueItemsModal extends Component {
const {
isOpen,
selectedCount,
canIgnore,
allPending
canIgnore
} = this.props;
const { remove, blocklist, skipredownload } = this.state;
const { remove, blacklist, skipredownload } = this.state;
return (
<Modal
@@ -97,50 +95,42 @@ class RemoveQueueItemsModal extends Component {
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormGroup>
<FormLabel>Remove From Download Client</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
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>
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
onChange={this.onBlocklistChange}
name="blacklist"
value={blacklist}
helpText="Prevents Readarr from automatically grabbing these files again"
onChange={this.onBlacklistChange}
/>
</FormGroup>
{
blocklist &&
blacklist &&
<FormGroup>
<FormLabel>
{translate('SkipRedownload')}
</FormLabel>
<FormLabel>Skip Redownload</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipredownload"
value={skipredownload}
helpText={translate('SkipredownloadHelpText')}
helpText="Prevents Readarr from trying download alternative releases for the removed items"
onChange={this.onSkipReDownloadChange}
/>
</FormGroup>
@@ -170,7 +160,6 @@ 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

@@ -5,7 +5,6 @@ import formatTime from 'Utilities/Date/formatTime';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './TimeleftCell.css';
function TimeleftCell(props) {
@@ -20,35 +19,35 @@ function TimeleftCell(props) {
timeFormat
} = props;
if (status === 'delay') {
if (status === 'Delay') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('DelayingDownloadUntilInterp', [date, time])}
title={`Delaying download until ${date} at ${time}`}
>
-
</TableRowCell>
);
}
if (status === 'downloadClientUnavailable') {
if (status === 'DownloadClientUnavailable') {
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
return (
<TableRowCell
className={styles.timeleft}
title={translate('RetryingDownloadInterp', [date, time])}
title={`Retrying download ${date} at ${time}`}
>
-
</TableRowCell>
);
}
if (!timeleft || status === 'completed' || status === 'failed') {
if (!timeleft) {
return (
<TableRowCell className={styles.timeleft}>
-

View File

@@ -1,27 +0,0 @@
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function AuthorMonitorNewItemsOptionsPopoverContent() {
return (
<DescriptionList>
<DescriptionListItem
title={translate('AllBooks')}
data={translate('DataNewAllBooks')}
/>
<DescriptionListItem
title={translate('NewBooks')}
data={translate('DataNewBooks')}
/>
<DescriptionListItem
title={translate('None')}
data={translate('DataNewNone')}
/>
</DescriptionList>
);
}
export default AuthorMonitorNewItemsOptionsPopoverContent;

View File

@@ -1,52 +1,45 @@
import React from 'react';
import Alert from 'Components/Alert';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate';
function AuthorMonitoringOptionsPopoverContent() {
return (
<>
<Alert>
{translate('MonitoringOptionsHelpText')}
</Alert>
<DescriptionList>
<DescriptionListItem
title={translate('AllBooks')}
data={translate('DataAllBooks')}
/>
<DescriptionList>
<DescriptionListItem
title="All Books"
data="Monitor all books"
/>
<DescriptionListItem
title={translate('FutureBooks')}
data={translate('DataFutureBooks')}
/>
<DescriptionListItem
title="Future Books"
data="Monitor books that have not released yet"
/>
<DescriptionListItem
title={translate('MissingBooks')}
data={translate('DataMissingBooks')}
/>
<DescriptionListItem
title="Missing Books"
data="Monitor books that do not have files or have not released yet"
/>
<DescriptionListItem
title={translate('ExistingBooks')}
data={translate('DataExistingBooks')}
/>
<DescriptionListItem
title="Existing Books"
data="Monitor books that have files or have not released yet"
/>
<DescriptionListItem
title={translate('FirstBook')}
data={translate('DataFirstBook')}
/>
<DescriptionListItem
title="First Book"
data="Monitor the first book. All other books will be ignored"
/>
<DescriptionListItem
title={translate('LatestBook')}
data={translate('DataLatestBook')}
/>
<DescriptionListItem
title="Latest Book"
data="Monitor the latest book and future books"
/>
<DescriptionListItem
title={translate('None')}
data={translate('DataNone')}
/>
</DescriptionList>
</>
<DescriptionListItem
title="None"
data="No books will be monitored"
/>
</DescriptionList>
);
}

View File

@@ -4,19 +4,16 @@ import React from 'react';
import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux';
import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes';
function App({ store, history }) {
return (
<DocumentTitle title={window.Readarr.instanceName}>
<DocumentTitle title="Readarr">
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ConnectedRouter>
</Provider>
</DocumentTitle>

View File

@@ -1,20 +1,18 @@
import PropTypes from 'prop-types';
import React from 'react';
import { Redirect, Route } from 'react-router-dom';
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector';
import AuthorEditorConnector from 'Author/Editor/AuthorEditorConnector';
import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector';
import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector';
import BookIndexConnector from 'Book/Index/BookIndexConnector';
import BookshelfConnector from 'Bookshelf/BookshelfConnector';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import AddNewItemConnector from 'Search/AddNewItemConnector';
import CustomFormatSettingsConnector from 'Settings/CustomFormats/CustomFormatSettingsConnector';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import ImportListSettingsConnector from 'Settings/ImportLists/ImportListSettingsConnector';
@@ -23,7 +21,7 @@ import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementCo
import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles';
import QualityConnector from 'Settings/Quality/QualityConnector';
import Quality from 'Settings/Quality/Quality';
import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
@@ -72,26 +70,19 @@ function AppRoutes(props) {
/>
}
<Route
path="/authors"
component={AuthorIndexConnector}
/>
<Route
path="/add/search"
component={AddNewItemConnector}
/>
<Route
exact={true}
path="/shelf"
component={BookshelfConnector}
path="/authoreditor"
component={AuthorEditorConnector}
/>
<Route
exact={true}
path="/books"
component={BookIndexConnector}
path="/bookshelf"
component={BookshelfConnector}
/>
<Route
@@ -133,8 +124,8 @@ function AppRoutes(props) {
/>
<Route
path="/activity/blocklist"
component={BlocklistConnector}
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/*
@@ -173,12 +164,7 @@ function AppRoutes(props) {
<Route
path="/settings/quality"
component={QualityConnector}
/>
<Route
path="/settings/customformats"
component={CustomFormatSettingsConnector}
component={Quality}
/>
<Route
@@ -221,11 +207,6 @@ function AppRoutes(props) {
component={UISettingsConnector}
/>
<Route
path="/settings/development"
component={DevelopmentSettingsConnector}
/>
{/*
System
*/}

View File

@@ -8,50 +8,11 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import UpdateChanges from 'System/Updates/UpdateChanges';
import translate from 'Utilities/String/translate';
import styles from './AppUpdatedModalContent.css';
function mergeUpdates(items, version, prevVersion) {
let installedIndex = items.findIndex((u) => u.version === version);
let installedPreviouslyIndex = items.findIndex((u) => u.version === prevVersion);
if (installedIndex === -1) {
installedIndex = 0;
}
if (installedPreviouslyIndex === -1) {
installedPreviouslyIndex = items.length;
} else if (installedPreviouslyIndex === installedIndex && items.length) {
installedPreviouslyIndex += 1;
}
const appliedUpdates = items.slice(installedIndex, installedPreviouslyIndex);
if (!appliedUpdates.length) {
return null;
}
const appliedChanges = { new: [], fixed: [] };
appliedUpdates.forEach((u) => {
if (u.changes) {
appliedChanges.new.push(... u.changes.new);
appliedChanges.fixed.push(... u.changes.fixed);
}
});
const mergedUpdate = Object.assign({}, appliedUpdates[0], { changes: appliedChanges });
if (!appliedChanges.new.length && !appliedChanges.fixed.length) {
mergedUpdate.changes = null;
}
return mergedUpdate;
}
function AppUpdatedModalContent(props) {
const {
version,
prevVersion,
isPopulated,
error,
items,
@@ -59,7 +20,7 @@ function AppUpdatedModalContent(props) {
onModalClose
} = props;
const update = mergeUpdates(items, version, prevVersion);
const update = items[0];
return (
<ModalContent onModalClose={onModalClose}>
@@ -77,9 +38,7 @@ function AppUpdatedModalContent(props) {
<div>
{
!update.changes &&
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
<div className={styles.maintenance}>Maintenance release</div>
}
{
@@ -90,12 +49,12 @@ function AppUpdatedModalContent(props) {
</div>
<UpdateChanges
title={translate('New')}
title="New"
changes={update.changes.new}
/>
<UpdateChanges
title={translate('Fixed')}
title="Fixed"
changes={update.changes.fixed}
/>
</div>
@@ -129,7 +88,6 @@ function AppUpdatedModalContent(props) {
AppUpdatedModalContent.propTypes = {
version: PropTypes.string.isRequired,
prevVersion: PropTypes.string,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,

View File

@@ -8,9 +8,8 @@ import AppUpdatedModalContent from './AppUpdatedModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.app.version,
(state) => state.app.prevVersion,
(state) => state.system.updates,
(version, prevVersion, updates) => {
(version, updates) => {
const {
isPopulated,
error,
@@ -19,7 +18,6 @@ function createMapStateToProps() {
return {
version,
prevVersion,
isPopulated,
error,
items

View File

@@ -1,49 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Readarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);

View File

@@ -99,7 +99,7 @@ class AuthorImage extends Component {
if (this.props.onError) {
this.props.onError();
}
};
}
onLoad = () => {
this.setState({
@@ -110,7 +110,7 @@ class AuthorImage extends Component {
if (this.props.onLoad) {
this.props.onLoad();
}
};
}
//
// Render
@@ -122,17 +122,9 @@ class AuthorImage extends Component {
placeholder,
size,
lazy,
overflow,
blurBackground
overflow
} = this.props;
const blurStyle = {
...style,
objectFit: 'fill',
filter: 'blur(8px)',
WebkitFilter: 'blur(8px)'
};
const {
url,
hasError,
@@ -176,26 +168,13 @@ class AuthorImage extends Component {
}
return (
<>
{
blurBackground ?
<img
style={blurStyle}
src={isLoaded ? url : placeholder}
onError={this.onError}
onLoad={this.onLoad}
/> :
null
}
<img
className={className}
style={style}
src={isLoaded ? url : placeholder}
onError={this.onError}
onLoad={this.onLoad}
/>
</>
<img
className={className}
style={style}
src={isLoaded ? url : placeholder}
onError={this.onError}
onLoad={this.onLoad}
/>
);
}
}
@@ -209,7 +188,6 @@ AuthorImage.propTypes = {
size: PropTypes.number.isRequired,
lazy: PropTypes.bool.isRequired,
overflow: PropTypes.bool.isRequired,
blurBackground: PropTypes.bool.isRequired,
onError: PropTypes.func,
onLoad: PropTypes.func
};
@@ -217,8 +195,7 @@ AuthorImage.propTypes = {
AuthorImage.defaultProps = {
size: 250,
lazy: true,
overflow: false,
blurBackground: false
overflow: false
};
export default AuthorImage;

View File

@@ -8,6 +8,7 @@ function AuthorPoster(props) {
return (
<AuthorImage
{...props}
coverType="poster"
placeholder={posterPlaceholder}
/>
);
@@ -18,8 +19,7 @@ AuthorPoster.propTypes = {
};
AuthorPoster.defaultProps = {
size: 250,
coverType: 'poster'
size: 250
};
export default AuthorPoster;

View File

@@ -8,5 +8,5 @@
.deleteFilesMessage {
margin-top: 20px;
color: var(--dangerColor);
color: $dangerColor;
}

View File

@@ -11,7 +11,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes';
import translate from 'Utilities/String/translate';
import styles from './DeleteAuthorModalContent.css';
class DeleteAuthorModalContent extends Component {
@@ -33,11 +32,11 @@ class DeleteAuthorModalContent extends Component {
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
}
onAddImportListExclusionChange = ({ value }) => {
this.setState({ addImportListExclusion: value });
};
}
onDeleteAuthorConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
@@ -46,7 +45,7 @@ class DeleteAuthorModalContent extends Component {
this.setState({ deleteFiles: false });
this.setState({ addImportListExclusion: false });
this.props.onDeletePress(deleteFiles, addImportListExclusion);
};
}
//
// Render
@@ -68,7 +67,7 @@ class DeleteAuthorModalContent extends Component {
const addImportListExclusion = this.state.addImportListExclusion;
let deleteFilesLabel = `Delete ${bookFileCount} Book Files`;
let deleteFilesHelpText = translate('DeleteFilesHelpText');
let deleteFilesHelpText = 'Delete the book files and author folder';
if (bookFileCount === 0) {
deleteFilesLabel = 'Delete Author Folder';
@@ -107,15 +106,13 @@ class DeleteAuthorModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>
{translate('AddListExclusion')}
</FormLabel>
<FormLabel>Add List Exclusion</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportListExclusion"
value={addImportListExclusion}
helpText={translate('AddImportListExclusionHelpText')}
helpText="Prevent author from being added to Readarr by Import lists"
kind={kinds.DANGER}
onChange={this.onAddImportListExclusionChange}
/>
@@ -124,9 +121,7 @@ class DeleteAuthorModalContent extends Component {
{
deleteFiles &&
<div className={styles.deleteFilesMessage}>
<div>
{translate('TheAuthorFolderAndAllOfItsContentWillBeDeleted', [path])}
</div>
<div>The author folder <strong>{path}</strong> and all of its content will be deleted.</div>
{
!!bookFileCount &&

View File

@@ -32,7 +32,7 @@ class DeleteAuthorModalContentConnector extends Component {
});
this.props.onModalClose(true);
};
}
//
// Render

View File

@@ -2,19 +2,152 @@
padding: 0;
}
.header {
position: relative;
width: 100%;
height: 310px;
}
.errorMessage {
margin-top: 20px;
text-align: center;
font-size: 20px;
}
.backdrop {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
background-size: cover;
}
.backdropOverlay {
position: absolute;
width: 100%;
height: 100%;
background: $black;
opacity: 0.7;
}
.headerContent {
display: flex;
padding: 30px;
width: 100%;
height: 100%;
color: $white;
}
.poster {
flex-shrink: 0;
margin-right: 35px;
height: 250px;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.metadataMessage {
color: var(--helpTextColor);
color: $helpTextColor;
text-align: center;
font-weight: 300;
font-size: 20px;
}
.titleRow {
position: relative;
display: flex;
justify-content: space-between;
flex: 0 0 auto;
}
.titleContainer {
display: flex;
margin-bottom: 5px;
}
.title {
font-weight: 300;
font-size: 50px;
line-height: 50px;
}
.toggleMonitoredContainer {
align-self: center;
margin-right: 10px;
}
.monitorToggleButton {
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 40px;
&:hover {
color: $iconButtonHoverLightColor;
}
}
.alternateTitlesIconContainer {
align-self: flex-end;
margin-left: 20px;
}
.artistNavigationButtons {
position: absolute;
right: 0;
white-space: nowrap;
}
.authorNavigationButton {
composes: button from '~Components/Link/IconButton.css';
margin-left: 5px;
width: 30px;
color: #e1e2e3;
white-space: nowrap;
&:hover {
color: $iconButtonHoverLightColor;
}
}
.details {
margin-bottom: 8px;
font-weight: 300;
font-size: 20px;
}
.runtime {
margin-right: 15px;
}
.detailsLabel {
composes: label from '~Components/Label.css';
margin: 5px 10px 5px 0;
}
.path,
.sizeOnDisk,
.qualityProfileName,
.links,
.tags {
margin-left: 8px;
font-weight: 300;
font-size: 17px;
}
.overview {
flex: 1 0 auto;
margin-top: 8px;
min-height: 0;
font-size: $intermediateFontSize;
}
.contentContainer {
padding: 20px;
}
@@ -22,7 +155,7 @@
.tabList {
margin: 0;
padding: 0;
border-bottom: 1px solid var(--lightGray);
border-bottom: 1px solid $lightGray;
}
.tab {
@@ -37,50 +170,25 @@
}
.selectedTab {
border-bottom: 4px solid var(--linkColor);
border-bottom: 4px solid $linkColor;
}
.tabContent {
margin-top: 20px;
}
.authorNavigationButtons {
position: absolute;
right: 0;
z-index: 1;
margin-top: 10px;
padding: 30px;
white-space: nowrap;
}
.authorUpButton,
.authorNavigationButton {
composes: button from '~Components/Link/IconButton.css';
margin-left: 5px;
width: 30px;
color: #e1e2e3;
white-space: nowrap;
&:hover {
color: var(--iconButtonHoverLightColor);
}
}
@media only screen and (max-width: $breakpointSmall) {
.contentContainer {
padding: 20px 0;
}
.authorNavigationButtons {
.headerContent {
padding: 15px;
}
}
.authorNavigationButtons {
margin-top: 5px;
}
.authorNavigationButton {
@media only screen and (max-width: $breakpointLarge) {
.poster {
display: none;
}
}

View File

@@ -1,37 +1,57 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import TextTruncate from 'react-text-truncate';
import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import SwipeHeaderConnector from 'Components/Swipe/SwipeHeaderConnector';
import { align, icons } from 'Helpers/Props';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import fonts from 'Styles/Variables/fonts';
import formatBytes from 'Utilities/Number/formatBytes';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
import AuthorDetailsHeaderConnector from './AuthorDetailsHeaderConnector';
import AuthorAlternateTitles from './AuthorAlternateTitles';
import AuthorDetailsLinks from './AuthorDetailsLinks';
import AuthorDetailsSeasonConnector from './AuthorDetailsSeasonConnector';
import AuthorDetailsSeriesConnector from './AuthorDetailsSeriesConnector';
import AuthorTagsConnector from './AuthorTagsConnector';
import styles from './AuthorDetails.css';
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) {
const fanartImage = _.find(images, { coverType: 'fanart' });
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
}
function getExpandedState(newState) {
return {
allExpanded: newState.allSelected,
@@ -54,114 +74,58 @@ class AuthorDetails extends Component {
isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: false,
isInteractiveImportModalOpen: false,
isMonitorOptionsModalOpen: false,
isEditorActive: false,
allExpanded: false,
allCollapsed: false,
expandedState: {},
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
selectedTabIndex: 0
};
}
//
// Control
setSelectedState = (items) => {
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((item) => {
const isItemSelected = selectedState[item.id];
if (isItemSelected) {
newSelectedState[item.id] = isItemSelected;
} else {
newSelectedState[item.id] = false;
}
});
const selectedCount = getSelectedIds(newSelectedState).length;
const newStateCount = Object.keys(newSelectedState).length;
let isAllSelected = false;
let isAllUnselected = false;
if (selectedCount === 0) {
isAllUnselected = true;
} else if (selectedCount === newStateCount) {
isAllSelected = true;
}
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
};
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
//
// Listeners
onOrganizePress = () => {
this.setState({ isOrganizeModalOpen: true });
};
}
onOrganizeModalClose = () => {
this.setState({ isOrganizeModalOpen: false });
};
}
onRetagPress = () => {
this.setState({ isRetagModalOpen: true });
};
}
onRetagModalClose = () => {
this.setState({ isRetagModalOpen: false });
};
}
onInteractiveImportPress = () => {
this.setState({ isInteractiveImportModalOpen: true });
};
}
onInteractiveImportModalClose = () => {
this.setState({ isInteractiveImportModalOpen: false });
};
}
onEditAuthorPress = () => {
this.setState({ isEditAuthorModalOpen: true });
};
}
onEditAuthorModalClose = () => {
this.setState({ isEditAuthorModalOpen: false });
};
}
onDeleteAuthorPress = () => {
this.setState({
isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: true
});
};
}
onDeleteAuthorModalClose = () => {
this.setState({ isDeleteAuthorModalOpen: false });
};
onMonitorOptionsPress = () => {
this.setState({ isMonitorOptionsModalOpen: true });
};
onMonitorOptionsClose = () => {
this.setState({ isMonitorOptionsModalOpen: false });
};
onBookEditorTogglePress = () => {
this.setState({ isEditorActive: !this.state.isEditorActive });
};
}
onExpandAllPress = () => {
const {
@@ -170,7 +134,7 @@ class AuthorDetails extends Component {
} = this.state;
this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
};
}
onExpandPress = (bookId, isExpanded) => {
this.setState((state) => {
@@ -184,32 +148,11 @@ class AuthorDetails extends Component {
return getExpandedState(newState);
});
};
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
};
onSelectedChange = (items, id, value, shiftKey = false) => {
this.setState((state) => {
return toggleSelected(state, items, id, value, shiftKey);
});
};
onSaveSelected = (changes) => {
this.props.onSaveSelected({
bookIds: this.getSelectedIds(),
...changes
});
};
}
onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index });
};
}
//
// Render
@@ -218,8 +161,18 @@ class AuthorDetails extends Component {
const {
id,
authorName,
ratings,
path,
statistics,
qualityProfileId,
monitored,
status,
overview,
links,
images,
alternateTitles,
tags,
isSaving,
isRefreshing,
isSearching,
isFetching,
@@ -233,31 +186,38 @@ class AuthorDetails extends Component {
hasBookFiles,
previousAuthor,
nextAuthor,
onMonitorTogglePress,
onRefreshPress,
onSearchPress,
isSaving,
saveError,
isDeleting,
deleteError,
statistics
onSearchPress
} = this.props;
const {
bookFileCount,
sizeOnDisk
} = statistics;
const {
isOrganizeModalOpen,
isRetagModalOpen,
isEditAuthorModalOpen,
isDeleteAuthorModalOpen,
isInteractiveImportModalOpen,
isMonitorOptionsModalOpen,
isEditorActive,
allSelected,
selectedState,
allExpanded,
allCollapsed,
expandedState,
selectedTabIndex
} = this.state;
const continuing = status === 'continuing';
let bookFilesCountMessage = 'No book files';
if (bookFileCount === 1) {
bookFilesCountMessage = '1 book file';
} else if (bookFileCount > 1) {
bookFilesCountMessage = `${bookFileCount} book files`;
}
let expandIcon = icons.EXPAND_INDETERMINATE;
if (allExpanded) {
@@ -266,48 +226,46 @@ class AuthorDetails extends Component {
expandIcon = icons.EXPAND;
}
const selectedBookIds = this.getSelectedIds();
return (
<PageContent title={authorName}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RefreshAndScan')}
label="Refresh & Scan"
iconName={icons.REFRESH}
spinningName={icons.REFRESH}
title={translate('RefreshInformationAndScanDisk')}
title="Refresh information and scan disk"
isSpinning={isRefreshing}
onPress={onRefreshPress}
/>
<PageToolbarButton
label={translate('SearchMonitored')}
label="Search Monitored"
iconName={icons.SEARCH}
isDisabled={!monitored || !hasMonitoredBooks || !hasBooks}
isSpinning={isSearching}
title={hasMonitoredBooks ? undefined : translate('HasMonitoredBooksNoMonitoredBooksForThisAuthor')}
title={hasMonitoredBooks ? undefined : 'No monitored books for this author'}
onPress={onSearchPress}
/>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('PreviewRename')}
label="Preview Rename"
iconName={icons.ORGANIZE}
isDisabled={!hasBookFiles}
onPress={this.onOrganizePress}
/>
<PageToolbarButton
label={translate('PreviewRetag')}
iconName={icons.RETAG}
isDisabled={!hasBookFiles}
onPress={this.onRetagPress}
/>
{/* <PageToolbarButton */}
{/* label="Preview Retag" */}
{/* iconName={icons.RETAG} */}
{/* isDisabled={!hasBookFiles} */}
{/* onPress={this.onRetagPress} */}
{/* /> */}
<PageToolbarButton
label={translate('ManualImport')}
label="Manual Import"
iconName={icons.INTERACTIVE}
onPress={this.onInteractiveImportPress}
/>
@@ -315,54 +273,21 @@ class AuthorDetails extends Component {
<PageToolbarSeparator />
<PageToolbarButton
label={translate('BookMonitoring')}
iconName={icons.MONITORED}
onPress={this.onMonitorOptionsPress}
/>
<PageToolbarButton
label={translate('Edit')}
label="Edit"
iconName={icons.EDIT}
onPress={this.onEditAuthorPress}
/>
<PageToolbarButton
label={translate('Delete')}
label="Delete"
iconName={icons.DELETE}
onPress={this.onDeleteAuthorPress}
/>
<PageToolbarSeparator />
{
isEditorActive ?
<PageToolbarButton
label={translate('BookList')}
iconName={icons.AUTHOR_CONTINUING}
onPress={this.onBookEditorTogglePress}
/> :
<PageToolbarButton
label={translate('BookEditor')}
iconName={icons.EDIT}
onPress={this.onBookEditorTogglePress}
/>
}
{
isEditorActive ?
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
onPress={this.onSelectAllPress}
/> :
null
}
</PageToolbarSection>
<PageToolbarSection alignContent={align.RIGHT}>
<PageToolbarButton
label={allExpanded ? translate('AllExpandedCollapseAll') : translate('AllExpandedExpandAll')}
label={allExpanded ? 'Collapse All' : 'Expand All'}
iconName={expandIcon}
onPress={this.onExpandAllPress}
/>
@@ -370,43 +295,234 @@ class AuthorDetails extends Component {
</PageToolbar>
<PageContentBody innerClassName={styles.innerContentBody}>
<SwipeHeaderConnector
className={styles.header}
nextLink={`/author/${nextAuthor.titleSlug}`}
nextComponent={(width) => <AuthorDetailsHeaderConnector authorId={nextAuthor.id} width={width} />}
prevLink={`/author/${previousAuthor.titleSlug}`}
prevComponent={(width) => <AuthorDetailsHeaderConnector authorId={previousAuthor.id} width={width} />}
currentComponent={(width) => <AuthorDetailsHeaderConnector authorId={id} width={width} />}
>
<div className={styles.authorNavigationButtons}>
<IconButton
className={styles.authorNavigationButton}
name={icons.ARROW_LEFT}
size={30}
title={translate('GoToInterp', [previousAuthor.authorName])}
to={`/author/${previousAuthor.titleSlug}`}
/>
<IconButton
className={styles.authorUpButton}
name={icons.ARROW_UP}
size={30}
title={translate('GoToAuthorListing')}
to={{
pathname: '/',
state: { restoreScrollPosition: true }
}}
/>
<IconButton
className={styles.authorNavigationButton}
name={icons.ARROW_RIGHT}
size={30}
title={translate('GoToInterp', [nextAuthor.authorName])}
to={`/author/${nextAuthor.titleSlug}`}
/>
<div className={styles.header}>
<div
className={styles.backdrop}
style={{
backgroundImage: `url(${getFanartUrl(images)})`
}}
>
<div className={styles.backdropOverlay} />
</div>
</SwipeHeaderConnector>
<div className={styles.headerContent}>
<AuthorPoster
className={styles.poster}
images={images}
size={250}
lazy={false}
/>
<div className={styles.info}>
<div className={styles.titleRow}>
<div className={styles.titleContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
isSaving={isSaving}
size={40}
onPress={onMonitorTogglePress}
/>
</div>
<div className={styles.title}>
{authorName}
</div>
{
!!alternateTitles.length &&
<div className={styles.alternateTitlesIconContainer}>
<Popover
anchor={
<Icon
name={icons.ALTERNATE_TITLES}
size={20}
/>
}
title="Alternate Titles"
body={<AuthorAlternateTitles alternateTitles={alternateTitles} />}
position={tooltipPositions.BOTTOM}
/>
</div>
}
</div>
<div className={styles.authorNavigationButtons}>
<IconButton
className={styles.authorNavigationButton}
name={icons.ARROW_LEFT}
size={30}
title={`Go to ${previousAuthor.authorName}`}
to={`/author/${previousAuthor.titleSlug}`}
/>
<IconButton
className={styles.authorNavigationButton}
name={icons.ARROW_UP}
size={30}
title={'Go to author listing'}
to={'/'}
/>
<IconButton
className={styles.authorNavigationButton}
name={icons.ARROW_RIGHT}
size={30}
title={`Go to ${nextAuthor.authorName}`}
to={`/author/${nextAuthor.titleSlug}`}
/>
</div>
</div>
<div className={styles.details}>
<div>
<HeartRating
rating={ratings.value}
iconSize={20}
/>
</div>
</div>
<div className={styles.detailsLabels}>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.FOLDER}
size={17}
/>
<span className={styles.path}>
{path}
</span>
</Label>
<Label
className={styles.detailsLabel}
title={bookFilesCountMessage}
size={sizes.LARGE}
>
<Icon
name={icons.DRIVE}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk || 0)
}
</span>
</Label>
<Label
className={styles.detailsLabel}
title="Quality Profile"
size={sizes.LARGE}
>
<Icon
name={icons.PROFILE}
size={17}
/>
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
/>
}
</span>
</Label>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={monitored ? icons.MONITORED : icons.UNMONITORED}
size={17}
/>
<span className={styles.qualityProfileName}>
{monitored ? 'Monitored' : 'Unmonitored'}
</span>
</Label>
<Label
className={styles.detailsLabel}
title={continuing ? 'More books are expected' : 'No additional books are expected'}
size={sizes.LARGE}
>
<Icon
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
size={17}
/>
<span className={styles.qualityProfileName}>
{continuing ? 'Continuing' : 'Deceased'}
</span>
</Label>
<Tooltip
anchor={
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.EXTERNAL_LINK}
size={17}
/>
<span className={styles.links}>
Links
</span>
</Label>
}
tooltip={
<AuthorDetailsLinks
links={links}
/>
}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
{
!!tags.length &&
<Tooltip
anchor={
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.TAGS}
size={17}
/>
<span className={styles.tags}>
Tags
</span>
</Label>
}
tooltip={<AuthorTagsConnector authorId={id} />}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
}
</div>
<div className={styles.overview}>
<TextTruncate
line={Math.floor(125 / (defaultFontSize * lineHeight))}
text={overview}
/>
</div>
</div>
</div>
</div>
<div className={styles.contentContainer}>
{
@@ -416,16 +532,12 @@ class AuthorDetails extends Component {
{
!isFetching && booksError &&
<div>
{translate('LoadingBooksFailed')}
</div>
<div>Loading books failed</div>
}
{
!isFetching && bookFilesError &&
<div>
{translate('LoadingBookFilesFailed')}
</div>
<div>Loading book files failed</div>
}
{
@@ -438,35 +550,35 @@ class AuthorDetails extends Component {
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('BooksTotal', [statistics.totalBookCount])}
Books
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('SeriesTotal', [series.length])}
Series
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('History')}
History
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('Search')}
Search
</Tab>
<Tab
className={styles.tab}
selectedClassName={styles.selectedTab}
>
{translate('FilesTotal', [statistics.bookFileCount])}
Files
</Tab>
{
@@ -483,11 +595,7 @@ class AuthorDetails extends Component {
<AuthorDetailsSeasonConnector
authorId={id}
isExpanded={true}
selectedState={selectedState}
onExpandPress={this.onExpandPress}
setSelectedState={this.setSelectedState}
onSelectedChange={this.onSelectedChange}
isEditorActive={isEditorActive}
/>
</TabPanel>
@@ -532,13 +640,14 @@ class AuthorDetails extends Component {
</TabPanel>
</Tabs>
}
</div>
<div className={styles.metadataMessage}>
{translate('TooManyBooks')}
<Link to='/settings/profiles'> {translate('MetadataProfile')} </Link>
Missing or too many books? Modify or create a new
<Link to='/settings/profiles'> Metadata Profile </Link>
or manually
<Link to={`/add/search?term=${encodeURIComponent(authorName)}`}> {translate('Search')} </Link>
<Link to={`/add/search?term=${encodeURIComponent(authorName)}`}> Search </Link>
for new items!
</div>
@@ -576,26 +685,7 @@ class AuthorDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
<MonitoringOptionsModal
isOpen={isMonitorOptionsModalOpen}
authorId={id}
onModalClose={this.onMonitorOptionsClose}
/>
</PageContentBody>
{
isEditorActive &&
<BookEditorFooter
bookIds={selectedBookIds}
selectedCount={selectedBookIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
onSaveSelected={this.onSaveSelected}
/>
}
</PageContent>
);
}
@@ -615,6 +705,7 @@ AuthorDetails.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
isSaving: PropTypes.bool.isRequired,
isRefreshing: PropTypes.bool.isRequired,
isSearching: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
@@ -628,20 +719,15 @@ AuthorDetails.propTypes = {
hasBookFiles: PropTypes.bool.isRequired,
previousAuthor: PropTypes.object.isRequired,
nextAuthor: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onMonitorTogglePress: PropTypes.func.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSaveSelected: PropTypes.func.isRequired
onSearchPress: PropTypes.func.isRequired
};
AuthorDetails.defaultProps = {
statistics: {},
tags: []
tags: [],
isSaving: false
};
export default AuthorDetails;

View File

@@ -6,15 +6,14 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { saveBookEditor } from 'Store/Actions/bookIndexActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { clearSeries, fetchSeries } from 'Store/Actions/seriesActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
@@ -22,8 +21,7 @@ import AuthorDetails from './AuthorDetails';
const selectBooks = createSelector(
(state) => state.books,
(state) => state.bookIndex,
(books, index) => {
(books) => {
const {
items,
isFetching,
@@ -31,13 +29,6 @@ const selectBooks = createSelector(
error
} = books;
const {
isSaving,
saveError,
isDeleting,
deleteError
} = index;
const hasBooks = !!items.length;
const hasMonitoredBooks = items.some((e) => e.monitored);
@@ -46,11 +37,7 @@ const selectBooks = createSelector(
isBooksPopulated: isPopulated,
booksError: error,
hasBooks,
hasMonitoredBooks,
isSaving,
saveError,
isDeleting,
deleteError
hasMonitoredBooks
};
}
);
@@ -107,9 +94,8 @@ function createMapStateToProps() {
selectBookFiles,
createAllAuthorSelector(),
createCommandsSelector(),
createDimensionsSelector(),
(titleSlug, books, series, bookFiles, allAuthors, commands, dimensions) => {
const sortedAuthor = _.orderBy(allAuthors, 'sortNameLastFirst');
(titleSlug, books, series, bookFiles, allAuthors, commands) => {
const sortedAuthor = _.orderBy(allAuthors, 'sortName');
const authorIndex = _.findIndex(sortedAuthor, { titleSlug });
const author = sortedAuthor[authorIndex];
@@ -122,11 +108,7 @@ function createMapStateToProps() {
isBooksPopulated,
booksError,
hasBooks,
hasMonitoredBooks,
isSaving,
saveError,
isDeleting,
deleteError
hasMonitoredBooks
} = books;
const {
@@ -186,10 +168,6 @@ function createMapStateToProps() {
isFetching,
isPopulated,
booksError,
isSaving,
saveError,
isDeleting,
deleteError,
seriesError,
bookFilesError,
hasBooks,
@@ -198,17 +176,17 @@ function createMapStateToProps() {
series: seriesItems,
hasBookFiles,
previousAuthor,
nextAuthor,
isSmallScreen: dimensions.isSmallScreen
nextAuthor
};
}
);
}
const mapDispatchToProps = {
fetchBooks,
clearBooks,
fetchSeries,
clearSeries,
saveBookEditor,
fetchBookFiles,
clearBookFiles,
toggleAuthorMonitored,
@@ -267,18 +245,20 @@ class AuthorDetailsConnector extends Component {
populate = () => {
const authorId = this.props.id;
this.props.fetchBooks({ authorId });
this.props.fetchSeries({ authorId });
this.props.fetchBookFiles({ authorId });
this.props.fetchQueueDetails({ authorId });
};
}
unpopulate = () => {
this.props.cancelFetchReleases();
this.props.clearBooks();
this.props.clearSeries();
this.props.clearBookFiles();
this.props.clearQueueDetails();
this.props.clearReleases();
};
}
//
// Listeners
@@ -288,25 +268,21 @@ class AuthorDetailsConnector extends Component {
authorId: this.props.id,
monitored
});
};
}
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.REFRESH_AUTHOR,
authorId: this.props.id
});
};
}
onSearchPress = () => {
this.props.executeCommand({
name: commandNames.AUTHOR_SEARCH,
authorId: this.props.id
});
};
onSaveSelected = (payload) => {
this.props.saveBookEditor(payload);
};
}
//
// Render
@@ -318,7 +294,6 @@ class AuthorDetailsConnector extends Component {
onMonitorTogglePress={this.onMonitorTogglePress}
onRefreshPress={this.onRefreshPress}
onSearchPress={this.onSearchPress}
onSaveSelected={this.onSaveSelected}
/>
);
}
@@ -332,9 +307,10 @@ AuthorDetailsConnector.propTypes = {
isRefreshing: PropTypes.bool.isRequired,
isRenamingFiles: PropTypes.bool.isRequired,
isRenamingAuthor: PropTypes.bool.isRequired,
fetchBooks: PropTypes.func.isRequired,
clearBooks: PropTypes.func.isRequired,
fetchSeries: PropTypes.func.isRequired,
clearSeries: PropTypes.func.isRequired,
saveBookEditor: PropTypes.func.isRequired,
fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired,
toggleAuthorMonitored: PropTypes.func.isRequired,

View File

@@ -1,148 +0,0 @@
.header {
position: relative;
width: 100%;
height: 310px;
}
.backdrop {
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
background-size: cover;
}
.backdropOverlay {
position: absolute;
width: 100%;
height: 100%;
background: var(--black);
opacity: 0.7;
}
.headerContent {
display: flex;
padding: 30px;
width: 100%;
height: 100%;
color: var(--white);
}
.poster {
flex-shrink: 0;
margin-right: 35px;
height: 250px;
}
.info {
display: flex;
flex-direction: column;
flex-grow: 1;
overflow: hidden;
}
.titleRow {
position: relative;
display: flex;
justify-content: space-between;
flex: 0 0 auto;
}
.titleContainer {
display: flex;
margin-top: -5px;
}
.title {
font-weight: 300;
font-size: 50px;
line-height: 60px;
}
.toggleMonitoredContainer {
align-self: center;
}
.monitorToggleButton {
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 40px;
&:hover {
color: var(--iconButtonHoverLightColor);
}
}
.alternateTitlesIconContainer {
align-self: flex-end;
margin-left: 20px;
}
.authorNavigationButtons {
position: absolute;
right: 0;
z-index: 1;
margin-top: 10px;
padding: 30px;
white-space: nowrap;
}
.authorUpButton,
.authorNavigationButton {
composes: button from '~Components/Link/IconButton.css';
margin-left: 5px;
width: 30px;
color: #e1e2e3;
white-space: nowrap;
&:hover {
color: var(--iconButtonHoverLightColor);
}
}
.details {
margin-bottom: 8px;
font-weight: 300;
font-size: 20px;
}
.detailsLabel {
composes: label from '~Components/Label.css';
margin: 5px 10px 5px 0;
}
.path,
.sizeOnDisk,
.qualityProfileName,
.links,
.tags {
margin-left: 8px;
font-weight: 300;
font-size: 17px;
}
.overview {
flex: 1 1 auto;
margin-top: 8px;
min-height: 0;
font-size: $intermediateFontSize;
}
@media only screen and (max-width: $breakpointSmall) {
.headerContent {
padding: 15px;
}
.title {
font-size: 30px;
line-height: 50px;
}
}
@media only screen and (max-width: $breakpointLarge) {
.poster {
display: none;
}
}

View File

@@ -1,340 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import AuthorPoster from 'Author/AuthorPoster';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Marquee from 'Components/Marquee';
import Measure from 'Components/Measure';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import Popover from 'Components/Tooltip/Popover';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import fonts from 'Styles/Variables/fonts';
import formatBytes from 'Utilities/Number/formatBytes';
import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate';
import AuthorAlternateTitles from './AuthorAlternateTitles';
import AuthorDetailsLinks from './AuthorDetailsLinks';
import AuthorTagsConnector from './AuthorTagsConnector';
import styles from './AuthorDetailsHeader.css';
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
function getFanartUrl(images) {
const fanartImage = images.find((x) => x.coverType === 'fanart');
if (fanartImage) {
// Remove protocol
return fanartImage.url.replace(/^https?:/, '');
}
}
class AuthorDetailsHeader extends Component {
//
// Lifecyle
constructor(props) {
super(props);
this.state = {
overviewHeight: 0,
titleWidth: 0
};
}
//
// Listeners
onOverviewMeasure = ({ height }) => {
this.setState({ overviewHeight: height });
};
onTitleMeasure = ({ width }) => {
this.setState({ titleWidth: width });
};
//
// Render
render() {
const {
id,
width,
authorName,
ratings,
path,
statistics,
qualityProfileId,
monitored,
status,
overview,
links,
images,
alternateTitles,
tags,
isSaving,
isSmallScreen,
onMonitorTogglePress
} = this.props;
const {
bookFileCount,
sizeOnDisk
} = statistics;
const {
overviewHeight,
titleWidth
} = this.state;
const marqueeWidth = titleWidth - (isSmallScreen ? 85 : 160);
const continuing = status === 'continuing';
let bookFilesCountMessage = translate('BookFilesCountMessage');
if (bookFileCount === 1) {
bookFilesCountMessage = '1 book file';
} else if (bookFileCount > 1) {
bookFilesCountMessage = `${bookFileCount} book files`;
}
return (
<div className={styles.header} style={{ width }} >
<div
className={styles.backdrop}
style={{
backgroundImage: `url(${getFanartUrl(images)})`
}}
>
<div className={styles.backdropOverlay} />
</div>
<div className={styles.headerContent}>
<AuthorPoster
className={styles.poster}
images={images}
size={250}
lazy={false}
/>
<div className={styles.info}>
<Measure
className={styles.titleRow}
onMeasure={this.onTitleMeasure}
>
<div className={styles.titleContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
isSaving={isSaving}
size={isSmallScreen ? 30: 40}
onPress={onMonitorTogglePress}
/>
</div>
<div className={styles.title} style={{ width: marqueeWidth }}>
<Marquee text={authorName} />
</div>
{
!!alternateTitles.length &&
<div className={styles.alternateTitlesIconContainer}>
<Popover
anchor={
<Icon
name={icons.ALTERNATE_TITLES}
size={20}
/>
}
title={translate('AlternateTitles')}
body={<AuthorAlternateTitles alternateTitles={alternateTitles} />}
position={tooltipPositions.BOTTOM}
/>
</div>
}
</div>
</Measure>
<div className={styles.details}>
<div>
<HeartRating
rating={ratings.value}
iconSize={20}
/>
</div>
</div>
<div className={styles.detailsLabels}>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.FOLDER}
size={17}
/>
<span className={styles.path}>
{path}
</span>
</Label>
<Label
className={styles.detailsLabel}
title={bookFilesCountMessage}
size={sizes.LARGE}
>
<Icon
name={icons.DRIVE}
size={17}
/>
<span className={styles.sizeOnDisk}>
{
formatBytes(sizeOnDisk || 0)
}
</span>
</Label>
<Label
className={styles.detailsLabel}
title={translate('QualityProfile')}
size={sizes.LARGE}
>
<Icon
name={icons.PROFILE}
size={17}
/>
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
/>
}
</span>
</Label>
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={monitored ? icons.MONITORED : icons.UNMONITORED}
size={17}
/>
<span className={styles.qualityProfileName}>
{monitored ? 'Monitored' : 'Unmonitored'}
</span>
</Label>
<Label
className={styles.detailsLabel}
title={continuing ? translate('ContinuingMoreBooksAreExpected') : translate('ContinuingNoAdditionalBooksAreExpected')}
size={sizes.LARGE}
>
<Icon
name={continuing ? icons.AUTHOR_CONTINUING : icons.AUTHOR_ENDED}
size={17}
/>
<span className={styles.qualityProfileName}>
{continuing ? 'Continuing' : 'Deceased'}
</span>
</Label>
<Tooltip
anchor={
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.EXTERNAL_LINK}
size={17}
/>
<span className={styles.links}>
Links
</span>
</Label>
}
tooltip={
<AuthorDetailsLinks
links={links}
/>
}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
{
!!tags.length &&
<Tooltip
anchor={
<Label
className={styles.detailsLabel}
size={sizes.LARGE}
>
<Icon
name={icons.TAGS}
size={17}
/>
<span className={styles.tags}>
Tags
</span>
</Label>
}
tooltip={<AuthorTagsConnector authorId={id} />}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
}
</div>
<Measure
onMeasure={this.onOverviewMeasure}
className={styles.overview}
>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={stripHtml(overview)}
/>
</Measure>
</div>
</div>
</div>
);
}
}
AuthorDetailsHeader.propTypes = {
id: PropTypes.number.isRequired,
width: PropTypes.number.isRequired,
authorName: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired,
path: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
overview: PropTypes.string,
links: PropTypes.arrayOf(PropTypes.object).isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
isSaving: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onMonitorTogglePress: PropTypes.func.isRequired
};
export default AuthorDetailsHeader;

View File

@@ -1,71 +0,0 @@
/* eslint max-params: 0 */
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import AuthorDetailsHeader from './AuthorDetailsHeader';
function createMapStateToProps() {
return createSelector(
(state) => state.authors,
createAuthorSelector(),
createDimensionsSelector(),
(authors, author, dimensions) => {
const alternateTitles = _.reduce(author.alternateTitles, (acc, alternateTitle) => {
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) {
acc.push(alternateTitle.title);
}
return acc;
}, []);
return {
...author,
isSaving: authors.isSaving,
alternateTitles,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
const mapDispatchToProps = {
toggleAuthorMonitored
};
class AuthorDetailsHeaderConnector extends Component {
//
// Listeners
onMonitorTogglePress = (monitored) => {
this.props.toggleAuthorMonitored({
authorId: this.props.authorId,
monitored
});
};
//
// Render
render() {
return (
<AuthorDetailsHeader
{...this.props}
onMonitorTogglePress={this.onMonitorTogglePress}
/>
);
}
}
AuthorDetailsHeaderConnector.propTypes = {
authorId: PropTypes.number.isRequired,
toggleAuthorMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorDetailsHeaderConnector);

View File

@@ -9,7 +9,6 @@ import NotFound from 'Components/NotFound';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import AuthorDetailsConnector from './AuthorDetailsConnector';
import styles from './AuthorDetails.css';
@@ -93,7 +92,7 @@ class AuthorDetailsPageConnector extends Component {
if (!titleSlug) {
return (
<NotFound
message={translate('SorryThatAuthorCannotBeFound')}
message="Sorry, that author cannot be found."
/>
);
}

View File

@@ -1,8 +1,8 @@
.bookType {
margin-bottom: 20px;
border: 1px solid var(--borderColor);
border: 1px solid $borderColor;
border-radius: 4px;
background-color: var(--cardBackgroundColor);
background-color: $white;
&:last-of-type {
margin-bottom: 0;
@@ -77,7 +77,7 @@
.books {
padding-top: 15px;
border-top: 1px solid var(--borderColor);
border-top: 1px solid $borderColor;
}
.collapseButtonContainer {
@@ -86,10 +86,10 @@
justify-content: center;
padding: 10px 15px;
width: 100%;
border-top: 1px solid var(--borderColor);
border-top: 1px solid $borderColor;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: var(--collapseButtonBackgroundColor);
background-color: #fafafa;
}
.collapseButtonIcon {

View File

@@ -4,7 +4,6 @@ import React, { Component } from 'react';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { sortDirections } from 'Helpers/Props';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import getToggledRange from 'Utilities/Table/getToggledRange';
import BookRowConnector from './BookRowConnector';
import styles from './AuthorDetailsSeason.css';
@@ -22,26 +21,6 @@ class AuthorDetailsSeason extends Component {
};
}
componentDidMount() {
this.props.setSelectedState(this.props.items);
}
componentDidUpdate(prevProps) {
const {
items,
sortKey,
sortDirection,
setSelectedState
} = this.props;
if (sortKey !== prevProps.sortKey ||
sortDirection !== prevProps.sortDirection ||
hasDifferentItemsOrOrder(prevProps.items, items)
) {
setSelectedState(items);
}
}
//
// Listeners
@@ -61,16 +40,7 @@ class AuthorDetailsSeason extends Component {
this.setState({ lastToggledBook: bookId });
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
const {
onSelectedChange,
items
} = this.props;
return onSelectedChange(items, id, value, shiftKey);
};
}
//
// Render
@@ -78,27 +48,20 @@ class AuthorDetailsSeason extends Component {
render() {
const {
items,
isEditorActive,
columns,
sortKey,
sortDirection,
onSortPress,
onTableOptionChange,
selectedState
onTableOptionChange
} = this.props;
let titleColumns = columns;
if (!isEditorActive) {
titleColumns = columns.filter((x) => x.name !== 'select');
}
return (
<div
className={styles.bookType}
>
<div className={styles.books}>
<Table
columns={titleColumns}
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
onSortPress={onSortPress}
@@ -113,9 +76,6 @@ class AuthorDetailsSeason extends Component {
columns={columns}
{...item}
onMonitorBookPress={this.onMonitorBookPress}
isEditorActive={isEditorActive}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
);
})
@@ -132,13 +92,9 @@ AuthorDetailsSeason.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
items: PropTypes.arrayOf(PropTypes.object).isRequired,
isEditorActive: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired,
setSelectedState: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onMonitorBookPress: PropTypes.func.isRequired,
uiSettings: PropTypes.object.isRequired

View File

@@ -4,22 +4,24 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setAuthorDetailsId, setAuthorDetailsSort } from 'Store/Actions/authorDetailsActions';
import { setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
import { setBooksSort, setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
import { executeCommand } from 'Store/Actions/commandActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import AuthorDetailsSeason from './AuthorDetailsSeason';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('books', 'authorDetails'),
(state, { label }) => label,
createClientSideCollectionSelector('books'),
createAuthorSelector(),
createCommandsSelector(),
createDimensionsSelector(),
createUISettingsSelector(),
(books, author, dimensions, uiSettings) => {
(label, books, author, commands, dimensions, uiSettings) => {
const booksInGroup = books.items;
@@ -45,39 +47,31 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
setAuthorDetailsId,
setAuthorDetailsSort,
toggleBooksMonitored,
setBooksTableOption,
dispatchSetBookSort: setBooksSort,
executeCommand
};
class AuthorDetailsSeasonConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.setAuthorDetailsId({ authorId: this.props.authorId });
}
//
// Listeners
onTableOptionChange = (payload) => {
this.props.setBooksTableOption(payload);
};
}
onSortPress = (sortKey) => {
this.props.setAuthorDetailsSort({ sortKey });
};
this.props.dispatchSetBookSort({ sortKey });
}
onMonitorBookPress = (bookIds, monitored) => {
this.props.toggleBooksMonitored({
bookIds,
monitored
});
};
}
//
// Render
@@ -98,8 +92,7 @@ AuthorDetailsSeasonConnector.propTypes = {
authorId: PropTypes.number.isRequired,
toggleBooksMonitored: PropTypes.func.isRequired,
setBooksTableOption: PropTypes.func.isRequired,
setAuthorDetailsId: PropTypes.func.isRequired,
setAuthorDetailsSort: PropTypes.func.isRequired,
dispatchSetBookSort: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

View File

@@ -1,8 +1,8 @@
.bookType {
margin-bottom: 20px;
border: 1px solid var(--borderColor);
border: 1px solid $borderColor;
border-radius: 4px;
background-color: var(--cardBackgroundColor);
background-color: $white;
&:last-of-type {
margin-bottom: 0;
@@ -77,7 +77,7 @@
.books {
padding-top: 15px;
border-top: 1px solid var(--borderColor);
border-top: 1px solid $borderColor;
}
.collapseButtonContainer {
@@ -86,10 +86,10 @@
justify-content: center;
padding: 10px 15px;
width: 100%;
border-top: 1px solid var(--borderColor);
border-top: 1px solid $borderColor;
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
background-color: var(--cardBackgroundColor);
background-color: #fafafa;
}
.collapseButtonIcon {
@@ -100,6 +100,12 @@
composes: actionButton;
margin-right: 15px;
/* position: absolute; */
/* top: 50%; */
/* left: 90%; */
/* margin-top: -12px; */
/* margin-left: -15px; */
}
.noBooks {

View File

@@ -8,7 +8,6 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { icons, sortDirections } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import getToggledRange from 'Utilities/Table/getToggledRange';
import BookRowConnector from './BookRowConnector';
import styles from './AuthorDetailsSeries.css';
@@ -73,7 +72,7 @@ class AuthorDetailsSeries extends Component {
} = this.props;
this.props.onExpandPress(id, !isExpanded);
};
}
onMonitorBookPress = (bookId, monitored, { shiftKey }) => {
const lastToggled = this.state.lastToggledBook;
@@ -91,13 +90,13 @@ class AuthorDetailsSeries extends Component {
this.setState({ lastToggledBook: bookId });
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
};
}
onMonitorSeriesPress = (monitored, { shiftKey }) => {
const bookIds = this.props.items.map((book) => book.id);
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
};
}
//
// Render
@@ -153,7 +152,7 @@ class AuthorDetailsSeries extends Component {
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? translate('IsExpandedHideBooks') : translate('IsExpandedShowBooks')}
title={isExpanded ? 'Hide books' : 'Show books'}
size={24}
/>
@@ -199,7 +198,7 @@ class AuthorDetailsSeries extends Component {
iconClassName={styles.collapseButtonIcon}
name={icons.COLLAPSE}
size={20}
title={translate('HideBooks')}
title="Hide books"
onPress={this.onExpandPress}
/>
</div>

View File

@@ -4,12 +4,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleBooksMonitored } from 'Store/Actions/bookActions';
import { setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { setSeriesSort, setSeriesTableOption } from 'Store/Actions/seriesActions';
import { setSeriesSort } from 'Store/Actions/seriesActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
// import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import AuthorDetailsSeries from './AuthorDetailsSeries';
@@ -69,7 +70,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
toggleBooksMonitored,
setSeriesTableOption,
setBooksTableOption,
dispatchSetSeriesSort: setSeriesSort,
executeCommand
};
@@ -80,19 +81,19 @@ class AuthorDetailsSeasonConnector extends Component {
// Listeners
onTableOptionChange = (payload) => {
this.props.setSeriesTableOption(payload);
};
this.props.setBooksTableOption(payload);
}
onSortPress = (sortKey) => {
this.props.dispatchSetSeriesSort({ sortKey });
};
}
onMonitorBookPress = (bookIds, monitored) => {
this.props.toggleBooksMonitored({
bookIds,
monitored
});
};
}
//
// Render
@@ -112,7 +113,7 @@ class AuthorDetailsSeasonConnector extends Component {
AuthorDetailsSeasonConnector.propTypes = {
authorId: PropTypes.number.isRequired,
toggleBooksMonitored: PropTypes.func.isRequired,
setSeriesTableOption: PropTypes.func.isRequired,
setBooksTableOption: PropTypes.func.isRequired,
dispatchSetSeriesSort: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};

View File

@@ -1,5 +1,7 @@
.title {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
white-space: nowrap;
}
.monitored {
@@ -8,22 +10,8 @@
width: 42px;
}
.position,
.rating,
.status {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 100px;
}
.releaseDate {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px;
}
.pageCount {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 80px;
}

View File

@@ -2,15 +2,27 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
import BookTitleLink from 'Book/BookTitleLink';
import Label from 'Components/Label';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import BookStatus from './BookStatus';
import { kinds, sizes } from 'Helpers/Props';
import styles from './BookRow.css';
function getBookCountKind(monitored, bookFileCount, bookCount) {
if (bookFileCount === bookCount && bookCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
class BookRow extends Component {
//
@@ -30,23 +42,23 @@ class BookRow extends Component {
onManualSearchPress = () => {
this.setState({ isDetailsModalOpen: true });
};
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
}
onEditBookPress = () => {
this.setState({ isEditBookModalOpen: true });
};
}
onEditBookModalClose = () => {
this.setState({ isEditBookModalOpen: false });
};
}
onMonitorBookPress = (monitored, options) => {
this.props.onMonitorBookPress(this.props.id, monitored, options);
};
}
//
// Render
@@ -56,6 +68,7 @@ class BookRow extends Component {
id,
authorId,
monitored,
statistics,
releaseDate,
title,
seriesTitle,
@@ -65,15 +78,14 @@ class BookRow extends Component {
isSaving,
authorMonitored,
titleSlug,
bookFiles,
isEditorActive,
isSelected,
onSelectedChange,
columns
} = this.props;
const bookFile = bookFiles[0];
const isAvailable = Date.parse(releaseDate) < new Date();
const {
bookCount,
bookFileCount,
totalBookCount
} = statistics;
return (
<TableRow>
@@ -88,18 +100,6 @@ class BookRow extends Component {
return null;
}
if (isEditorActive && name === 'select') {
return (
<TableSelectCell
key={name}
id={id}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'monitored') {
return (
<TableRowCell
@@ -145,7 +145,7 @@ class BookRow extends Component {
return (
<TableRowCell
key={name}
className={styles.position}
className={styles.title}
>
{position || ''}
</TableRowCell>
@@ -154,10 +154,7 @@ class BookRow extends Component {
if (name === 'rating') {
return (
<TableRowCell
key={name}
className={styles.rating}
>
<TableRowCell key={name}>
{
<StarRating
rating={ratings.value}
@@ -171,7 +168,6 @@ class BookRow extends Component {
if (name === 'releaseDate') {
return (
<RelativeDateCellConnector
className={styles.releaseDate}
key={name}
date={releaseDate}
/>
@@ -182,7 +178,6 @@ class BookRow extends Component {
return (
<TableRowCell
key={name}
className={styles.pageCount}
>
{pageCount || ''}
</TableRowCell>
@@ -195,11 +190,15 @@ class BookRow extends Component {
key={name}
className={styles.status}
>
<BookStatus
isAvailable={isAvailable}
monitored={monitored}
bookFile={bookFile}
/>
<Label
title={`${totalBookCount} books total. ${bookFileCount} books with files.`}
kind={getBookCountKind(monitored, bookFileCount, bookCount)}
size={sizes.MEDIUM}
>
{
<span>{bookFileCount} / {bookCount}</span>
}
</Label>
</TableRowCell>
);
}
@@ -229,18 +228,22 @@ BookRow.propTypes = {
releaseDate: PropTypes.string,
title: PropTypes.string.isRequired,
seriesTitle: PropTypes.string.isRequired,
position: PropTypes.string,
position: PropTypes.number,
pageCount: PropTypes.number,
ratings: PropTypes.object.isRequired,
titleSlug: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired,
bookFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
statistics: PropTypes.object.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onMonitorBookPress: PropTypes.func.isRequired
};
BookRow.defaultProps = {
statistics: {
bookCount: 0,
bookFileCount: 0
}
};
export default BookRow;

View File

@@ -2,38 +2,17 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createBookFileSelector from 'Store/Selectors/createBookFileSelector';
import BookRow from './BookRow';
const selectBookFiles = createSelector(
(state) => state.bookFiles,
(bookFiles) => {
const {
items
} = bookFiles;
const bookFileDict = items.reduce((acc, file) => {
const bookId = file.bookId;
if (!acc.hasOwnProperty(bookId)) {
acc[bookId] = [];
}
acc[bookId].push(file);
return acc;
}, {});
return bookFileDict;
}
);
function createMapStateToProps() {
return createSelector(
createAuthorSelector(),
selectBookFiles,
(state, { id }) => id,
(author = {}, bookFiles, bookId) => {
createBookFileSelector(),
(author = {}, bookFile) => {
return {
authorMonitored: author.monitored,
bookFiles: bookFiles[bookId] ?? []
bookFilePath: bookFile ? bookFile.path : null
};
}
);

View File

@@ -1,78 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import BookQuality from 'Book/BookQuality';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './BookStatus.css';
function BookStatus(props) {
const {
isAvailable,
monitored,
bookFile
} = props;
const hasBookFile = !!bookFile;
if (hasBookFile) {
const quality = bookFile.quality;
return (
<div className={styles.center}>
<BookQuality
title={quality.quality.name}
size={bookFile.size}
quality={quality}
isMonitored={monitored}
isCutoffNotMet={bookFile.qualityCutoffNotMet}
/>
</div>
);
}
if (!monitored) {
return (
<div className={styles.center}>
<Label
title={translate('NotMonitored')}
kind={kinds.WARNING}
>
{translate('NotMonitored')}
</Label>
</div>
);
}
if (isAvailable) {
return (
<div className={styles.center}>
<Label
title={translate('BookAvailableButMissing')}
kind={kinds.DANGER}
>
{translate('Missing')}
</Label>
</div>
);
}
return (
<div className={styles.center}>
<Label
title={translate('NotAvailable')}
kind={kinds.INFO}
>
{translate('NotAvailable')}
</Label>
</div>
);
}
BookStatus.propTypes = {
isAvailable: PropTypes.bool,
monitored: PropTypes.bool.isRequired,
bookFile: PropTypes.object
};
export default BookStatus;

View File

@@ -16,7 +16,7 @@ class EditAuthorModalConnector extends Component {
onModalClose = () => {
this.props.clearPendingChanges({ section: 'author' });
this.props.onModalClose();
};
}
//
// Render

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup';
@@ -16,7 +15,6 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import Popover from 'Components/Tooltip/Popover';
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './EditAuthorModalContent.css';
class EditAuthorModalContent extends Component {
@@ -48,13 +46,13 @@ class EditAuthorModalContent extends Component {
onSavePress(false);
}
};
}
onMoveAuthorPress = () => {
this.setState({ isConfirmMoveModalOpen: false });
this.props.onSavePress(true);
};
}
//
// Render
@@ -74,7 +72,6 @@ class EditAuthorModalContent extends Component {
const {
monitored,
monitorNewItems,
qualityProfileId,
metadataProfileId,
path,
@@ -90,48 +87,19 @@ class EditAuthorModalContent extends Component {
<ModalBody>
<Form {...otherProps}>
<FormGroup>
<FormLabel>
{translate('Monitored')}
</FormLabel>
<FormLabel>Monitored</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredHelpText')}
helpText="Download monitored books from this author"
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('MonitorNewItems')}
<Popover
anchor={
<Icon
className={styles.labelIcon}
name={icons.INFO}
/>
}
title={translate('MonitorNewItems')}
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
position={tooltipPositions.RIGHT}
/>
</FormLabel>
<FormInputGroup
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
name="monitorNewItems"
helpText={translate('MonitorNewItemsHelpText')}
{...monitorNewItems}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('QualityProfile')}
</FormLabel>
<FormLabel>Quality Profile</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
@@ -154,7 +122,7 @@ class EditAuthorModalContent extends Component {
name={icons.INFO}
/>
}
title={translate('MetadataProfile')}
title="Metadata Profile"
body={<AuthorMetadataProfilePopoverContent />}
position={tooltipPositions.RIGHT}
/>
@@ -164,7 +132,7 @@ class EditAuthorModalContent extends Component {
<FormInputGroup
type={inputTypes.METADATA_PROFILE_SELECT}
name="metadataProfileId"
helpText={translate('MetadataProfileIdHelpText')}
helpText="Changes will take place on next author refresh"
includeNone={true}
{...metadataProfileId}
onChange={onInputChange}
@@ -173,9 +141,7 @@ class EditAuthorModalContent extends Component {
}
<FormGroup>
<FormLabel>
{translate('Path')}
</FormLabel>
<FormLabel>Path</FormLabel>
<FormInputGroup
type={inputTypes.PATH}
@@ -186,9 +152,7 @@ class EditAuthorModalContent extends Component {
</FormGroup>
<FormGroup>
<FormLabel>
{translate('Tags')}
</FormLabel>
<FormLabel>Tags</FormLabel>
<FormInputGroup
type={inputTypes.TAG}

View File

@@ -39,7 +39,6 @@ function createMapStateToProps() {
const authorSettings = _.pick(author, [
'monitored',
'monitorNewItems',
'qualityProfileId',
'metadataProfileId',
'path',
@@ -83,14 +82,14 @@ class EditAuthorModalContentConnector extends Component {
onInputChange = ({ name, value }) => {
this.props.dispatchSetAuthorValue({ name, value });
};
}
onSavePress = (moveFiles) => {
this.props.dispatchSaveAuthor({
id: this.props.authorId,
moveFiles
});
};
}
//
// Render

View File

@@ -6,25 +6,3 @@
margin-top: 20px;
margin-bottom: 10px;
}
.searchForNewBookLabelContainer {
display: flex;
margin-top: 2px;
}
.searchForNewBookLabel {
margin-right: 8px;
font-weight: normal;
}
.searchForNewBookContainer {
composes: container from '~Components/Form/CheckInput.css';
flex: 0 1 0;
}
.searchForNewBookInput {
composes: input from '~Components/Form/CheckInput.css';
margin-top: 0;
}

View File

@@ -1,7 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React from 'react';
import Alert from 'Components/Alert';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
@@ -11,114 +10,58 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import { icons, kinds } from 'Helpers/Props';
import styles from './RetagAuthorModalContent.css';
class RetagAuthorModalContent extends Component {
function RetagAuthorModalContent(props) {
const {
authorNames,
onModalClose,
onRetagAuthorPress
} = props;
//
// Lifecycle
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Retag Selected Author
</ModalHeader>
constructor(props, context) {
super(props, context);
<ModalBody>
<Alert>
Tip: To preview the tags that will be written... select "Cancel" then click any author name and use the
<Icon
className={styles.retagIcon}
name={icons.RETAG}
/>
</Alert>
this.state = {
updateCovers: false,
embedMetadata: false
};
}
<div className={styles.message}>
Are you sure you want to re-tag all files in the {authorNames.length} selected author?
</div>
<ul>
{
authorNames.map((authorName) => {
return (
<li key={authorName}>
{authorName}
</li>
);
})
}
</ul>
</ModalBody>
//
// Listeners
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
onCheckInputChange = ({ name, value }) => {
this.setState({ [name]: value });
};
onRetagAuthorPress = () => {
this.props.onRetagAuthorPress(this.state.updateCovers, this.state.embedMetadata);
};
//
// Render
render() {
const {
authorNames,
onModalClose
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Retag Selected Author
</ModalHeader>
<ModalBody>
<Alert>
Tip: To preview the tags that will be written... select "Cancel" then click any author name and use the
<Icon
className={styles.retagIcon}
name={icons.RETAG}
/>
</Alert>
<div className={styles.message}>
Are you sure you want to re-tag all files in the {authorNames.length} selected author?
</div>
<ul>
{
authorNames.map((authorName) => {
return (
<li key={authorName}>
{authorName}
</li>
);
})
}
</ul>
</ModalBody>
<ModalFooter>
<label className={styles.searchForNewBookLabelContainer}>
<span className={styles.searchForNewBookLabel}>
Update Covers
</span>
<CheckInput
containerClassName={styles.searchForNewBookContainer}
className={styles.searchForNewBookInput}
name="updateCovers"
value={this.state.updateCovers}
onChange={this.onCheckInputChange}
/>
</label>
<label className={styles.searchForNewBookLabelContainer}>
<span className={styles.searchForNewBookLabel}>
Embed Metadata
</span>
<CheckInput
containerClassName={styles.searchForNewBookContainer}
className={styles.searchForNewBookInput}
name="embedMetadata"
value={this.state.embedMetadata}
onChange={this.onCheckInputChange}
/>
</label>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRetagAuthorPress}
>
Retag
</Button>
</ModalFooter>
</ModalContent>
);
}
<Button
kind={kinds.DANGER}
onPress={onRetagAuthorPress}
>
Retag
</Button>
</ModalFooter>
</ModalContent>
);
}
RetagAuthorModalContent.propTypes = {

View File

@@ -36,16 +36,14 @@ class RetagAuthorModalContentConnector extends Component {
//
// Listeners
onRetagAuthorPress = (updateCovers, embedMetadata) => {
onRetagAuthorPress = () => {
this.props.executeCommand({
name: commandNames.RETAG_AUTHOR,
authorIds: this.props.authorIds,
updateCovers,
embedMetadata
authorIds: this.props.authorIds
});
this.props.onModalClose(true);
};
}
//
// Render

View File

@@ -0,0 +1,279 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import RetagAuthorModal from './AudioTags/RetagAuthorModal';
import AuthorEditorFilterModalConnector from './AuthorEditorFilterModalConnector';
import AuthorEditorFooter from './AuthorEditorFooter';
import AuthorEditorRowConnector from './AuthorEditorRowConnector';
import OrganizeAuthorModal from './Organize/OrganizeAuthorModal';
class AuthorEditor extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
isOrganizingAuthorModalOpen: false,
isRetaggingAuthorModalOpen: false
};
}
componentDidUpdate(prevProps) {
const {
isDeleting,
deleteError
} = this.props;
const hasFinishedDeleting = prevProps.isDeleting &&
!isDeleting &&
!deleteError;
if (hasFinishedDeleting) {
this.onSelectAllChange({ value: false });
}
}
//
// 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);
});
}
onSaveSelected = (changes) => {
this.props.onSaveSelected({
authorIds: this.getSelectedIds(),
...changes
});
}
onOrganizeAuthorPress = () => {
this.setState({ isOrganizingAuthorModalOpen: true });
}
onOrganizeAuthorModalClose = (organized) => {
this.setState({ isOrganizingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
onRetagAuthorPress = () => {
this.setState({ isRetaggingAuthorModalOpen: true });
}
onRetagAuthorModalClose = (organized) => {
this.setState({ isRetaggingAuthorModalOpen: false });
if (organized === true) {
this.onSelectAllChange({ value: false });
}
}
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
columns,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
isSaving,
saveError,
isDeleting,
deleteError,
isOrganizingAuthor,
isRetaggingAuthor,
onTableOptionChange,
onSortPress,
onFilterSelect
} = this.props;
const {
allSelected,
allUnselected,
selectedState
} = this.state;
const selectedAuthorIds = this.getSelectedIds();
return (
<PageContent title="Author Editor">
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label="Options"
iconName={icons.TABLE}
/>
</TableOptionsModalWrapper>
<PageToolbarSeparator />
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={AuthorEditorFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<div>{getErrorMessage(error, 'Failed to load author from API')}</div>
}
{
!error && isPopulated && !!items.length &&
<div>
<Table
columns={columns}
sortKey={sortKey}
sortDirection={sortDirection}
selectAll={true}
allSelected={allSelected}
allUnselected={allUnselected}
onSortPress={onSortPress}
onSelectAllChange={this.onSelectAllChange}
>
<TableBody>
{
items.map((item) => {
return (
<AuthorEditorRowConnector
key={item.id}
{...item}
columns={columns}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/>
);
})
}
</TableBody>
</Table>
</div>
}
{
!error && isPopulated && !items.length &&
<NoAuthor totalItems={totalItems} />
}
</PageContentBody>
<AuthorEditorFooter
authorIds={selectedAuthorIds}
selectedCount={selectedAuthorIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingAuthor={isOrganizingAuthor}
isRetaggingAuthor={isRetaggingAuthor}
columns={columns}
showMetadataProfile={columns.find((column) => column.name === 'metadataProfileId').isVisible}
onSaveSelected={this.onSaveSelected}
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
onRetagAuthorPress={this.onRetagAuthorPress}
/>
<OrganizeAuthorModal
isOpen={this.state.isOrganizingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onOrganizeAuthorModalClose}
/>
<RetagAuthorModal
isOpen={this.state.isRetaggingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onRetagAuthorModalClose}
/>
</PageContent>
);
}
}
AuthorEditor.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};
export default AuthorEditor;

View File

@@ -0,0 +1,97 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort, setAuthorEditorTableOption } from 'Store/Actions/authorEditorActions';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchRootFolders } from 'Store/Actions/settingsActions';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import AuthorEditor from './AuthorEditor';
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('authors', 'authorEditor'),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
(author, isOrganizingAuthor, isRetaggingAuthor) => {
return {
isOrganizingAuthor,
isRetaggingAuthor,
...author
};
}
);
}
const mapDispatchToProps = {
dispatchSetAuthorEditorSort: setAuthorEditorSort,
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
dispatchSetAuthorEditorTableOption: setAuthorEditorTableOption,
dispatchSaveAuthorEditor: saveAuthorEditor,
dispatchFetchRootFolders: fetchRootFolders,
dispatchExecuteCommand: executeCommand
};
class AuthorEditorConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchRootFolders();
}
//
// Listeners
onSortPress = (sortKey) => {
this.props.dispatchSetAuthorEditorSort({ sortKey });
}
onFilterSelect = (selectedFilterKey) => {
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
}
onTableOptionChange = (payload) => {
this.props.dispatchSetAuthorEditorTableOption(payload);
}
onSaveSelected = (payload) => {
this.props.dispatchSaveAuthorEditor(payload);
}
onMoveSelected = (payload) => {
this.props.dispatchExecuteCommand({
name: commandNames.MOVE_AUTHOR,
...payload
});
}
//
// Render
render() {
return (
<AuthorEditor
{...this.props}
onSortPress={this.onSortPress}
onFilterSelect={this.onFilterSelect}
onSaveSelected={this.onSaveSelected}
onTableOptionChange={this.onTableOptionChange}
/>
);
}
}
AuthorEditorConnector.propTypes = {
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
dispatchSetAuthorEditorTableOption: PropTypes.func.isRequired,
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired,
dispatchExecuteCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorEditorConnector);

View File

@@ -1,24 +1,24 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setBookFilter } from 'Store/Actions/bookIndexActions';
import { setAuthorEditorFilter } from 'Store/Actions/authorEditorActions';
function createMapStateToProps() {
return createSelector(
(state) => state.books.items,
(state) => state.bookIndex.filterBuilderProps,
(state) => state.authors.items,
(state) => state.authorEditor.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'bookIndex'
customFilterType: 'authorEditor'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setBookFilter
dispatchSetFilter: setAuthorEditorFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

View File

@@ -1,23 +1,11 @@
.footer {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.dropdownContainer {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
}
.inputContainer {
flex: 1;
margin-right: 20px;
min-width: 150px;
}
.buttonContainer {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
@@ -36,14 +24,12 @@
composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px;
margin-bottom: 10px;
height: 35px;
}
.deleteSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-bottom: 10px;
margin-left: 50px;
height: 35px;
}
@@ -62,10 +48,6 @@
}
@media only screen and (max-width: $breakpointSmall) {
.dropdownContainer {
display: block;
}
.inputContainer {
margin-right: 0;
}
@@ -79,7 +61,6 @@
}
.buttons {
display: block;
justify-content: space-between;
}

View File

@@ -2,14 +2,12 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import AuthorEditorFooterLabel from './AuthorEditorFooterLabel';
import DeleteAuthorModal from './Delete/DeleteAuthorModal';
import TagsModal from './Tags/TagsModal';
@@ -27,7 +25,6 @@ class AuthorEditorFooter extends Component {
this.state = {
monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
@@ -48,7 +45,6 @@ class AuthorEditorFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
@@ -80,7 +76,7 @@ class AuthorEditorFooter extends Component {
default:
this.props.onSaveSelected({ [name]: value });
}
};
}
onApplyTagsPress = (tags, applyTags) => {
this.setState({
@@ -92,23 +88,23 @@ class AuthorEditorFooter extends Component {
tags,
applyTags
});
};
}
onDeleteSelectedPress = () => {
this.setState({ isDeleteAuthorModalOpen: true });
};
}
onDeleteAuthorModalClose = () => {
this.setState({ isDeleteAuthorModalOpen: false });
};
}
onTagsPress = () => {
this.setState({ isTagsModalOpen: true });
};
}
onTagsModalClose = () => {
this.setState({ isTagsModalOpen: false });
};
}
onSaveRootFolderPress = () => {
this.setState({
@@ -117,7 +113,7 @@ class AuthorEditorFooter extends Component {
});
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
};
}
onMoveAuthorPress = () => {
this.setState({
@@ -129,7 +125,7 @@ class AuthorEditorFooter extends Component {
rootFolderPath: this.state.destinationRootFolder,
moveFiles: true
});
};
}
//
// Render
@@ -142,15 +138,16 @@ class AuthorEditorFooter extends Component {
isDeleting,
isOrganizingAuthor,
isRetaggingAuthor,
columns,
onOrganizeAuthorPress,
onRetagAuthorPress
} = this.props;
const {
monitored,
monitorNewItems,
qualityProfileId,
metadataProfileId,
bookFolder,
rootFolderPath,
savingTags,
isTagsModalOpen,
@@ -165,101 +162,142 @@ class AuthorEditorFooter extends Component {
{ key: 'unmonitored', value: 'Unmonitored' }
];
const bookFolderOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'yes', value: 'Yes' },
{ key: 'no', value: 'No' }
];
return (
<PageContentFooter>
<div className={styles.footer}>
<div className={styles.dropdownContainer}>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label={translate('MonitorAuthor')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label="Monitor Author"
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label={translate('MonitorNewItems')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
<MonitorNewItemsSelectInput
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
if (!isVisible) {
return null;
}
<div className={styles.inputContainer}>
<AuthorEditorFooterLabel
label={translate('QualityProfile')}
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
if (name === 'qualityProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Quality Profile"
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
<div
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('MetadataProfile')}
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
if (name === 'metadataProfileId') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Metadata Profile"
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
includeNone={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<MetadataProfileSelectInputConnector
name="metadataProfileId"
value={metadataProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
<div
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
if (name === 'bookFolder') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Book Folder"
isSaving={isSaving && bookFolder !== NO_CHANGE}
/>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
</div>
<SelectInput
name="bookFolder"
value={bookFolder}
values={bookFolderOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
);
}
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
isSaving={false}
/>
if (name === 'path') {
return (
<div
key={name}
className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label="Root Folder"
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<div className={styles.buttons}>
<RootFolderSelectInputConnector
name="rootFolderPath"
value={rootFolderPath}
includeNoChange={true}
isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange}
/>
</div>
);
}
return null;
})
}
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={`${selectedCount} Author(s) Selected`}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
@@ -267,7 +305,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onOrganizeAuthorPress}
>
{translate('RenameFiles')}
Rename Files
</SpinnerButton>
<SpinnerButton
@@ -277,7 +315,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onRetagAuthorPress}
>
{translate('WriteMetadataTags')}
Write Metadata Tags
</SpinnerButton>
<SpinnerButton
@@ -286,20 +324,19 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={this.onTagsPress}
>
{translate('SetReadarrTags')}
Set Readarr Tags
</SpinnerButton>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
</div>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton>
</div>
</div>
</div>
@@ -339,6 +376,7 @@ AuthorEditorFooter.propTypes = {
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired

View File

@@ -1,6 +1,5 @@
.TrackActionsCell {
.bookFolder {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
white-space: nowrap;
width: 150px;
}

View File

@@ -2,11 +2,13 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AuthorNameLink from 'Author/AuthorNameLink';
import AuthorStatusCell from 'Author/Index/Table/AuthorStatusCell';
import CheckInput from 'Components/Form/CheckInput';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow';
import TagListConnector from 'Components/TagListConnector';
import formatBytes from 'Utilities/Number/formatBytes';
import styles from './AuthorEditorRow.css';
class AuthorEditorRow extends Component {
@@ -16,7 +18,7 @@ class AuthorEditorRow extends Component {
onBookFolderChange = () => {
// Mock handler to satisfy `onChange` being required for `CheckInput`.
//
};
}
//
// Render
@@ -25,8 +27,10 @@ class AuthorEditorRow extends Component {
const {
id,
status,
titleSlug,
foreignAuthorId,
authorName,
authorType,
bookFolder,
monitored,
metadataProfile,
qualityProfile,
@@ -34,7 +38,9 @@ class AuthorEditorRow extends Component {
statistics,
tags,
columns,
isSaving,
isSelected,
onAuthorMonitoredPress,
onSelectedChange
} = this.props;
@@ -61,8 +67,11 @@ class AuthorEditorRow extends Component {
return (
<AuthorStatusCell
key={name}
authorType={authorType}
monitored={monitored}
status={status}
isSaving={isSaving}
onMonitoredPress={onAuthorMonitoredPress}
/>
);
}
@@ -71,9 +80,10 @@ class AuthorEditorRow extends Component {
return (
<TableRowCell
key={name}
className={styles.title}
>
<AuthorNameLink
titleSlug={titleSlug}
foreignAuthorId={foreignAuthorId}
authorName={authorName}
/>
</TableRowCell>
@@ -96,6 +106,22 @@ class AuthorEditorRow extends Component {
);
}
if (name === 'bookFolder') {
return (
<TableRowCell
key={name}
className={styles.bookFolder}
>
<CheckInput
name="bookFolder"
value={bookFolder}
isDisabled={true}
onChange={this.onBookFolderChange}
/>
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
@@ -133,8 +159,11 @@ class AuthorEditorRow extends Component {
AuthorEditorRow.propTypes = {
id: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
foreignAuthorId: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
authorName: PropTypes.string.isRequired,
authorType: PropTypes.string,
bookFolder: PropTypes.string,
monitored: PropTypes.bool.isRequired,
metadataProfile: PropTypes.object.isRequired,
qualityProfile: PropTypes.object.isRequired,
@@ -142,7 +171,9 @@ AuthorEditorRow.propTypes = {
statistics: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onAuthorMonitoredPress: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired
};

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