Compare commits

..

1 Commits

Author SHA1 Message Date
Mark McDowall 53e7718fcf Fixed: Release profiles not saving if Must (Not) Contain is empty
(cherry picked from commit 0abd52d6beba82f9c9af0d6469aa1a7157128537)
2021-10-05 16:25:49 +00:00
860 changed files with 10473 additions and 23497 deletions
+8 -4
View File
@@ -19,10 +19,10 @@ indent_size = 4
dotnet_sort_system_directives_first = true dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary # Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:warning dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:warning dotnet_style_qualification_for_event = false:refactoring
# Indentation preferences # Indentation preferences
csharp_indent_block_contents = true csharp_indent_block_contents = true
@@ -32,6 +32,10 @@ csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true csharp_indent_switch_labels = true
csharp_indent_labels = flush_left 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.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _ dotnet_naming_style.instance_field_style.required_prefix = _
+6 -6
View File
@@ -1,13 +1,14 @@
name: Bug Report name: Bug Report
title: "[BUG]: "
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first' 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'] labels: ['Type: Bug', 'Status: Needs Triage']
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? 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. description: Please search to see if an issue already exists for the bug you encountered.
options: options:
- label: I have searched the existing open and closed issues - label: I have searched the existing issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
@@ -42,14 +43,12 @@ body:
- **Docker Install**: Yes - **Docker Install**: Yes
- **Using Reverse Proxy**: No - **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related) - **Browser**: Firefox 90 (If UI related)
- **Database**: Sqlite 3.36.0
value: | value: |
- OS: - OS:
- Readarr: - Readarr:
- Docker Install: - Docker Install:
- Using Reverse Proxy: - Using Reverse Proxy:
- Browser: - Browser:
- Database:
render: markdown render: markdown
validations: validations:
required: true required: true
@@ -65,11 +64,12 @@ body:
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
label: Trace Logs? label: Anything else?
description: | description: |
Trace Logs (https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files) Trace Logs (https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files)
Links? References? Anything that will give us more context about the issue you are encountering!
***Generally speaking, all bug reports must have trace logs provided.*** ***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. 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: validations:
required: true required: true
+3 -2
View File
@@ -1,13 +1,14 @@
name: Feature Request name: Feature Request
title: "[FEAT]: "
description: 'Suggest an idea for Readarr' description: 'Suggest an idea for Readarr'
labels: ['Type: Feature Request', 'Status: Needs Triage'] labels: ['Type: Feature Request', 'Status: Needs Triage']
body: body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? 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. description: Please search to see if an issue already exists for the feature you are requesting.
options: options:
- label: I have searched the existing open and closed issues - label: I have searched the existing issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+5
View File
@@ -0,0 +1,5 @@
{
"files.associations": {
"*.yaml": "home-assistant"
}
}
-132
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
+44 -6
View File
@@ -1,13 +1,51 @@
# 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. 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). This file is updated on an ad-hoc basis, for the latest details please see the [contributing wiki page](https://wiki.servarr.com/readarr/contributing).
## Documentation ## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better.
Setup guides, [FAQ](https://wiki.servarr.com/readarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/readarr) the better. ## Development ##
## Development ### 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 12.X.X or higher)
- [Yarn](https://yarnpkg.com/)
- .NET Core 5.0.
See the [Wiki Page](https://wiki.servarr.com/readarr/contributing) ### Getting started ###
1. Fork Readarr
2. Clone the repository into your development machine. [*info*](https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository-from-github)
3. Install the required Node Packages `yarn install`
4. Start gulp to monitor your dev environment for any changes that need post processing using `yarn start` command.
5. Build the project in Visual Studio, Setting startup project to `Readarr.Console` and framework to `net5.0`
6. Debug the project in Visual Studio
7. 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.
+18 -39
View File
@@ -1,77 +1,56 @@
# Readarr # 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) [![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://hub.docker.com/r/hotio/readarr)
[![Docker Pulls](https://img.shields.io/docker/pulls/hotio/readarr)](https://wiki.servarr.com/readarr/installation#docker) [![Backers on Open Collective](https://opencollective.com/readarr/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/readarr/sponsors/badge.svg)](#sponsors)
[![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)
### 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 currently in beta testing and is generally still in a work in progress. Features may be broken, incomplete, or cause spontaneous combustion.
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. 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. 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.
## 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. * Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Automatically detects new books * Automatically detects new books
* Can scan your existing library and download any missing books * Can scan your existing library and download any missing books
* Automatic failed download handling will try another release if one fails * 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 * 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 * Fully configurable book renaming
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated * Full integration with SABnzbd and NZBGet
* Full integration with Calibre (add to library, conversion) (Requires Calibre Content Server) * Full integration with Calibre (add to library, conversion)
* And a beautiful UI * And a beautiful UI
## Support ## 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)
Note: GitHub Issues are for Bugs and Feature Requests Only Note: GitHub Issues are for Bugs and Feature Requests Only
[![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)
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Readarr/Readarr/issues) [![GitHub - Bugs and Feature Requests Only](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)
## Contributors & Developers ## Contributors
[API Documentation](https://readarr.com/docs/api/) 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>
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)
## Backers ## 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 ## 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) 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
<img src="https://opencollective.com/readarr/tiers/mega-sponsor.svg?width=890"></a>
[![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>
### License ### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022 * Copyright 2010-2021
-8
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.
+125 -284
View File
@@ -7,19 +7,15 @@ variables:
outputFolder: './_output' outputFolder: './_output'
artifactsFolder: './_artifacts' artifactsFolder: './_artifacts'
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn majorVersion: '0.1.0'
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '0.1.1'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
readarrVersion: '$(majorVersion).$(minorVersion)' readarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(readarrVersion)' buildName: '$(Build.SourceBranchName).$(readarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.302' dotnetVersion: '5.0.302'
innoVersion: '6.2.0' yarnCacheFolder: $(Pipeline.Workspace)/.yarn
windowsImage: 'windows-2022' nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
trigger: trigger:
branches: branches:
@@ -46,7 +42,7 @@ stages:
matrix: matrix:
Windows: Windows:
osName: 'Windows' osName: 'Windows'
imageName: ${{ variables.windowsImage }} imageName: 'windows-2019'
enableAnalysis: 'false' enableAnalysis: 'false'
pool: pool:
@@ -66,13 +62,16 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}" BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi fi
displayName: Extra Platform Support displayName: Enable FreeBSD Support
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props' key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
@@ -82,29 +81,31 @@ stages:
displayName: Build Readarr Backend displayName: Build Readarr Backend
env: env:
NUGET_PACKAGES: $(nugetCacheFolder) NUGET_PACKAGES: $(nugetCacheFolder)
- powershell: Get-ChildItem _output\net6.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item - powershell: Get-ChildItem _output\net5.0*,_output\*.Update\* -Recurse | Where { $_.Fullname -notlike "*\publish\*" -and $_.attributes -notlike "*directory*" } | Remove-Item
displayName: Clean up intermediate output displayName: Clean up intermediate output
- publish: $(outputFolder) - task: PublishPipelineArtifact@1
artifact: '$(osName)Backend' inputs:
path: $(outputFolder)
artifact: '$(osName)Backend'
artifactType: 'pipeline'
parallel: true
parallelCount: 100
displayName: Publish Backend displayName: Publish Backend
- publish: '$(testsFolder)/net6.0/win-x64/publish' - publish: '$(testsFolder)/net5.0/win-x64/publish'
artifact: win-x64-tests artifact: WindowsCoreTests
displayName: Publish win-x64 Test Package displayName: Publish Windows Test Package
- publish: '$(testsFolder)/net6.0/linux-x64/publish' - publish: '$(testsFolder)/net5.0/linux-x64/publish'
artifact: linux-x64-tests artifact: LinuxCoreTests
displayName: Publish linux-x64 Test Package displayName: Publish Linux Test Package
- publish: '$(testsFolder)/net6.0/linux-x86/publish' - publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
artifact: linux-x86-tests artifact: LinuxMuslCoreTests
displayName: Publish linux-x86 Test Package displayName: Publish Linux Musl Test Package
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish' - publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
artifact: linux-musl-x64-tests artifact: FreebsdCoreTests
displayName: Publish linux-musl-x64 Test Package displayName: Publish FreeBSD Test Package
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish' - publish: '$(testsFolder)/net5.0/osx-x64/publish'
artifact: freebsd-x64-tests artifact: MacCoreTests
displayName: Publish freebsd-x64 Test Package displayName: Publish MacOS Test Package
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
- stage: Build_Backend_Other - stage: Build_Backend_Other
displayName: Build Backend (Other OS) displayName: Build Backend (Other OS)
@@ -115,11 +116,11 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
imageName: ${{ variables.linuxImage }} imageName: 'ubuntu-18.04'
enableAnalysis: 'true' enableAnalysis: 'true'
Mac: Mac:
osName: 'Mac' osName: 'Mac'
imageName: ${{ variables.macImage }} imageName: 'macos-10.14'
enableAnalysis: 'false' enableAnalysis: 'false'
pool: pool:
@@ -136,29 +137,25 @@ stages:
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
- bash: | - bash: |
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}" BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS echo "BSD already enabled"
else
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi fi
displayName: Extra Platform Support displayName: Enable FreeBSD Support
- task: Cache@2 - task: Cache@2
inputs: inputs:
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props' key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
path: $(nugetCacheFolder) path: $(nugetCacheFolder)
displayName: Cache NuGet packages displayName: Cache NuGet packages
- bash: ./build.sh --backend --enable-extra-platforms - bash: ./build.sh --backend --enable-bsd
displayName: Build Readarr Backend displayName: Build Readarr Backend
env: env:
NUGET_PACKAGES: $(nugetCacheFolder) NUGET_PACKAGES: $(nugetCacheFolder)
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${OUTPUTFOLDER} -depth -empty -type d -exec rm -r "{}" \;
find ${TESTSFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
find ${TESTSFOLDER} -depth -empty -type d -exec rm -r "{}" \;
displayName: Clean up intermediate output
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
displayName: Frontend displayName: Frontend
@@ -169,13 +166,13 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
imageName: ${{ variables.linuxImage }} imageName: 'ubuntu-18.04'
Mac: Mac:
osName: 'Mac' osName: 'Mac'
imageName: ${{ variables.macImage }} imageName: 'macos-10.14'
Windows: Windows:
osName: 'Windows' osName: 'Windows'
imageName: ${{ variables.windowsImage }} imageName: 'windows-2019'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
@@ -212,7 +209,7 @@ stages:
- job: Windows_Installer - job: Windows_Installer
displayName: Create Installer displayName: Create Installer
pool: pool:
vmImage: ${{ variables.windowsImage }} vmImage: 'windows-2019'
steps: steps:
- checkout: self - checkout: self
fetchDepth: 1 fetchDepth: 1
@@ -228,11 +225,16 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages
displayName: Create Packages
- bash: | - bash: |
./build.sh --packages --installer setup/inno/ISCC.exe setup/readarr.iss //DFramework=net5.0 //DRuntime=win-x86
cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe cp setup/output/Readarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
cp setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe displayName: Create .NET Core Windows installer
displayName: Create Installers - bash: |
setup/inno/ISCC.exe setup/readarr.iss //DFramework=net5.0 //DRuntime=win-x64
cp setup/output/Readarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create .NET Core Windows installer
- publish: $(Build.ArtifactStagingDirectory) - publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller' artifact: 'WindowsInstaller'
displayName: Publish Installer displayName: Publish Installer
@@ -245,7 +247,7 @@ stages:
- job: Other_Packages - job: Other_Packages
displayName: Create Standard Packages displayName: Create Standard Packages
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: 'ubuntu-18.04'
steps: steps:
- checkout: self - checkout: self
fetchDepth: 1 fetchDepth: 1
@@ -261,120 +263,90 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-extra-platforms - bash: ./build.sh --packages --enable-bsd
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "fpcalc" -exec chmod a+x {} \;
find . -name "Readarr" -exec chmod a+x {} \; find . -name "Readarr" -exec chmod a+x {} \;
find . -name "Readarr.Update" -exec chmod a+x {} \; find . -name "Readarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits displayName: Set executable bits
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create win-x64 zip displayName: Create Windows Core zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create win-x86 zip displayName: Create Windows x86 Core zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-x64 app displayName: Create MacOS Core app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-x64 tar displayName: Create MacOS Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/macos/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-arm64 app displayName: Create Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2
displayName: Create osx-arm64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create linux-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-x64 tar displayName: Create Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-x86 tar displayName: Create ARM32 Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x86.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x86/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-arm tar displayName: Create Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2
displayName: Create linux-arm64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-arm64 tar displayName: Create ARM64 Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create freebsd-x64 tar displayName: Create FreeBSD Core Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
tarCompression: 'gz' tarCompression: 'gz'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
- publish: $(Build.ArtifactStagingDirectory) - publish: $(Build.ArtifactStagingDirectory)
artifact: 'Packages' artifact: 'Packages'
displayName: Publish Packages displayName: Publish Packages
@@ -420,22 +392,22 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'osx-x64' testName: 'MacCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }} imageName: 'macos-10.14'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'win-x64' testName: 'WindowsCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }} imageName: 'windows-2019'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'linux-x64' testName: 'LinuxCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }} imageName: 'ubuntu-18.04'
FreebsdCore: FreebsdCore:
osName: 'Linux' osName: 'Linux'
testName: 'freebsd-x64' testName: 'FreebsdCore'
poolName: 'FreeBSD' poolName: 'FreeBSD'
imageName: imageName:
@@ -454,11 +426,15 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)-tests' artifactName: '$(testName)Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
chmod a+x _tests/fpcalc
displayName: Make fpcalc Executable
condition: and(succeeded(), or(eq(variables['osName'], 'Mac'), eq(variables['testName'], 'LinuxCore')))
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \; - bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows')) condition: and(succeeded(), ne(variables['osName'], 'Windows'))
@@ -482,15 +458,11 @@ stages:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: 'ubuntu-18.04'
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
@@ -498,15 +470,9 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -529,57 +495,6 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
artifactName: LinuxCoreTests
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-Tests'
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Readarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration - stage: Integration
displayName: Integration displayName: Integration
@@ -591,18 +506,18 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'osx-x64' testName: 'MacCore'
imageName: ${{ variables.macImage }} imageName: 'macos-10.14'
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'win-x64' testName: 'WindowsCore'
imageName: ${{ variables.windowsImage }} imageName: 'windows-2019'
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'linux-x64' testName: 'LinuxCore'
imageName: ${{ variables.linuxImage }} imageName: 'ubuntu-18.04'
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
pool: pool:
@@ -618,7 +533,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)-tests' artifactName: '$(testName)Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -648,66 +563,6 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
variables:
pattern: 'Readarr.*.linux-core-x64.tar.gz'
Readarr__Postgres__Host: 'localhost'
Readarr__Postgres__Port: '5432'
Readarr__Postgres__User: 'readarr'
Readarr__Postgres__Password: 'readarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Readarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=readarr \
-e POSTGRES_USER=readarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
workspace: workspace:
@@ -723,14 +578,14 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'freebsd-x64-tests' artifactName: 'FreebsdCoreTests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: Packages artifactName: Packages
itemPattern: '**/$(pattern)' itemPattern: '/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- bash: | - bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -757,17 +612,13 @@ stages:
strategy: strategy:
matrix: matrix:
alpine: alpine:
testName: 'linux-musl-x64' testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Readarr.*.linux-musl-core-x64.tar.gz' pattern: 'Readarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Readarr.*.linux-core-x86.tar.gz'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: 'ubuntu-18.04'
container: $[ variables['containerImage'] ] container: $[ variables['containerImage'] ]
@@ -775,15 +626,9 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -829,18 +674,15 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
artifactName: 'linux-x64' imageName: 'ubuntu-18.04'
imageName: ${{ variables.linuxImage }}
pattern: 'Readarr.*.linux-core-x64.tar.gz' pattern: 'Readarr.*.linux-core-x64.tar.gz'
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64' imageName: 'macos-10.14'
imageName: ${{ variables.macImage }}
pattern: 'Readarr.*.osx-core-x64.tar.gz' pattern: 'Readarr.*.osx-core-x64.tar.gz'
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64' imageName: 'windows-2019'
imageName: ${{ variables.windowsImage }}
pattern: 'Readarr.*.windows-core-x64.zip' pattern: 'Readarr.*.windows-core-x64.zip'
pool: pool:
@@ -856,7 +698,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(artifactName)-tests' artifactName: '$(osName)CoreTests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -907,10 +749,10 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
imageName: ${{ variables.linuxImage }} imageName: 'ubuntu-18.04'
Windows: Windows:
osName: 'Windows' osName: 'Windows'
imageName: ${{ variables.windowsImage }} imageName: 'windows-2019'
pool: pool:
vmImage: $(imageName) vmImage: $(imageName)
steps: steps:
@@ -939,7 +781,7 @@ stages:
displayName: Frontend displayName: Frontend
condition: eq(variables['System.PullRequest.IsFork'], 'False') condition: eq(variables['System.PullRequest.IsFork'], 'False')
pool: pool:
vmImage: ${{ variables.windowsImage }} vmImage: windows-2019
steps: steps:
- checkout: self # Need history for Sonar analysis - checkout: self # Need history for Sonar analysis
- task: SonarCloudPrepare@1 - task: SonarCloudPrepare@1
@@ -962,7 +804,7 @@ stages:
variables: variables:
disable.coverage.autogenerate: 'true' disable.coverage.autogenerate: 'true'
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ubuntu-18.04
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
@@ -1000,8 +842,8 @@ stages:
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: | - bash: |
./build.sh --backend -f net6.0 -r linux-x64 ./build.sh --backend -f net5.0 -r linux-x64
TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage TEST_DIR=_tests/net5.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
displayName: Coverage Unit Tests displayName: Coverage Unit Tests
env: env:
NUGET_PACKAGES: $(nugetCacheFolder) NUGET_PACKAGES: $(nugetCacheFolder)
@@ -1034,7 +876,7 @@ stages:
- job: - job:
displayName: Discord Notification displayName: Discord Notification
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: 'ubuntu-18.04'
steps: steps:
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
continueOnError: true continueOnError: true
@@ -1050,4 +892,3 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+38 -100
View File
@@ -27,22 +27,15 @@ UpdateVersionNumber()
fi fi
} }
EnableExtraPlatformsInSDK() EnableBsdSupport()
{ {
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g') #todo enable sdk with
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" #SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then # BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
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 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 sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
sed -i'' -e "s^<ExcludedRuntimeFrameworkPairs>\(.*\)</ExcludedRuntimeFrameworkPairs>^<ExcludedRuntimeFrameworkPairs>\1;freebsd-x64:net472</ExcludedRuntimeFrameworkPairs>^g" src/Directory.Build.props
fi fi
} }
@@ -136,7 +129,7 @@ PackageLinux()
echo "Adding Readarr.Mono to UpdatePackage" echo "Adding Readarr.Mono to UpdatePackage"
cp $folder/Readarr.Mono.* $folder/Readarr.Update cp $folder/Readarr.Mono.* $folder/Readarr.Update
if [ "$framework" = "net6.0" ]; then if [ "$framework" = "net5.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
fi fi
@@ -147,13 +140,12 @@ PackageLinux()
PackageMacOS() PackageMacOS()
{ {
local framework="$1" 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"
echo "Removing Service helpers" echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.* rm -f $folder/ServiceUninstall.*
@@ -164,7 +156,7 @@ PackageMacOS()
echo "Adding Readarr.Mono to UpdatePackage" echo "Adding Readarr.Mono to UpdatePackage"
cp $folder/Readarr.Mono.* $folder/Readarr.Update cp $folder/Readarr.Mono.* $folder/Readarr.Update
if [ "$framework" = "net6.0" ]; then if [ "$framework" = "net5.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update cp $folder/Mono.Posix.NETStandard.* $folder/Readarr.Update
cp $folder/libMonoPosixHelper.* $folder/Readarr.Update cp $folder/libMonoPosixHelper.* $folder/Readarr.Update
fi fi
@@ -175,11 +167,10 @@ PackageMacOS()
PackageMacOSApp() PackageMacOSApp()
{ {
local framework="$1" 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 rm -rf $folder
mkdir -p $folder mkdir -p $folder
@@ -187,7 +178,7 @@ PackageMacOSApp()
mkdir -p $folder/Readarr.app/Contents/MacOS mkdir -p $folder/Readarr.app/Contents/MacOS
echo "Copying Binaries" 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" echo "Removing Update Folder"
rm -r $folder/Readarr.app/Contents/MacOS/Readarr.Update rm -r $folder/Readarr.app/Contents/MacOS/Readarr.Update
@@ -234,38 +225,12 @@ Package()
PackageWindows "$framework" "$runtime" PackageWindows "$framework" "$runtime"
;; ;;
osx) osx)
PackageMacOS "$framework" "$runtime" PackageMacOS "$framework"
PackageMacOSApp "$framework" "$runtime" PackageMacOSApp "$framework"
;; ;;
esac 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() PackageTests()
{ {
local framework="$1" local framework="$1"
@@ -297,10 +262,8 @@ if [ $# -eq 0 ]; then
BACKEND=YES BACKEND=YES
FRONTEND=YES FRONTEND=YES
PACKAGES=YES PACKAGES=YES
INSTALLER=NO
LINT=YES LINT=YES
ENABLE_EXTRA_PLATFORMS=NO ENABLE_BSD=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -312,12 +275,8 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd|--enable-extra-platforms) --enable-bsd)
ENABLE_EXTRA_PLATFORMS=YES ENABLE_BSD=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument shift # past argument
;; ;;
-r|--runtime) -r|--runtime)
@@ -338,10 +297,6 @@ case $key in
PACKAGES=YES PACKAGES=YES
shift # past argument shift # past argument
;; ;;
--installer)
INSTALLER=YES
shift # past argument
;;
--lint) --lint)
LINT=YES LINT=YES
shift # past argument shift # past argument
@@ -361,30 +316,24 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
EnableExtraPlatforms EnableBsdSupport
fi fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then then
PackageTests "net6.0" "win-x64" PackageTests "net5.0" "win-x64"
PackageTests "net6.0" "win-x86" PackageTests "net5.0" "win-x86"
PackageTests "net6.0" "linux-x64" PackageTests "net5.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net5.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64" PackageTests "net5.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
PackageTests "net6.0" "freebsd-x64" PackageTests "net5.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
@@ -413,30 +362,19 @@ then
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
then then
Package "net6.0" "win-x64" Package "net5.0" "win-x64"
Package "net6.0" "win-x86" Package "net5.0" "win-x86"
Package "net6.0" "linux-x64" Package "net5.0" "linux-x64"
Package "net6.0" "linux-musl-x64" Package "net5.0" "linux-musl-x64"
Package "net6.0" "linux-arm64" Package "net5.0" "linux-arm64"
Package "net6.0" "linux-musl-arm64" Package "net5.0" "linux-musl-arm64"
Package "net6.0" "linux-arm" Package "net5.0" "linux-arm"
Package "net6.0" "linux-musl-arm" Package "net5.0" "osx-x64"
Package "net6.0" "osx-x64" if [ "$ENABLE_BSD" = "YES" ];
Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
then then
Package "net6.0" "freebsd-x64" Package "net5.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"
fi fi
fi fi
if [ "$INSTALLER" = "YES" ];
then
InstallInno
BuildInstaller "net6.0" "win-x64"
BuildInstaller "net6.0" "win-x86"
RemoveInno
fi
+5
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
View File
@@ -0,0 +1 @@
8
+12
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
Vendored Executable
+24
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".
Vendored Executable
+1
View File
@@ -0,0 +1 @@
nzbdrone_bin/* opt/NzbDrone
Vendored Executable
+13
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 $@
+1 -4
View File
@@ -39,7 +39,6 @@ module.exports = {
plugins: [ plugins: [
'filenames', 'filenames',
'react', 'react',
'react-hooks',
'simple-import-sort', 'simple-import-sort',
'import' 'import'
], ],
@@ -309,9 +308,7 @@ module.exports = {
'react/react-in-jsx-scope': 2, 'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2, 'react/self-closing-comp': 2,
'react/sort-comp': 2, 'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2, 'react/jsx-wrap-multilines': 2
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error'
}, },
overrides: [ overrides: [
{ {
@@ -19,9 +19,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import BlocklistRowConnector from './BlocklistRowConnector'; import BlacklistRowConnector from './BlacklistRowConnector';
class Blocklist extends Component { class Blacklist extends Component {
// //
// Lifecycle // Lifecycle
@@ -103,8 +103,8 @@ class Blocklist extends Component {
columns, columns,
totalRecords, totalRecords,
isRemoving, isRemoving,
isClearingBlocklistExecuting, isClearingBlacklistExecuting,
onClearBlocklistPress, onClearBlacklistPress,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -121,7 +121,7 @@ class Blocklist extends Component {
const selectedIds = this.getSelectedIds(); const selectedIds = this.getSelectedIds();
return ( return (
<PageContent title={translate('Blocklist')}> <PageContent title={translate('Blacklist')}>
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
@@ -135,8 +135,8 @@ class Blocklist extends Component {
<PageToolbarButton <PageToolbarButton
label={translate('Clear')} label={translate('Clear')}
iconName={icons.CLEAR} iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting} isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlocklistPress} onPress={onClearBlacklistPress}
/> />
</PageToolbarSection> </PageToolbarSection>
@@ -162,14 +162,14 @@ class Blocklist extends Component {
{ {
!isAnyFetching && !!error && !isAnyFetching && !!error &&
<div> <div>
{translate('UnableToLoadBlocklist')} {translate('UnableToLoadBlacklist')}
</div> </div>
} }
{ {
isAllPopulated && !error && !items.length && isAllPopulated && !error && !items.length &&
<div> <div>
{translate('NoHistoryBlocklist')} No history blacklist
</div> </div>
} }
@@ -188,7 +188,7 @@ class Blocklist extends Component {
{ {
items.map((item) => { items.map((item) => {
return ( return (
<BlocklistRowConnector <BlacklistRowConnector
key={item.id} key={item.id}
isSelected={selectedState[item.id] || false} isSelected={selectedState[item.id] || false}
columns={columns} columns={columns}
@@ -224,7 +224,7 @@ class Blocklist extends Component {
} }
} }
Blocklist.propTypes = { Blacklist.propTypes = {
isAuthorFetching: PropTypes.bool.isRequired, isAuthorFetching: PropTypes.bool.isRequired,
isAuthorPopulated: PropTypes.bool.isRequired, isAuthorPopulated: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
@@ -234,9 +234,9 @@ Blocklist.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number, totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired, isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired, onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired onClearBlacklistPress: PropTypes.func.isRequired
}; };
export default Blocklist; export default Blacklist;
@@ -4,34 +4,34 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import withCurrentPage from 'Components/withCurrentPage'; 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 { executeCommand } from 'Store/Actions/commandActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Blocklist from './Blocklist'; import Blacklist from './Blacklist';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.blocklist, (state) => state.blacklist,
(state) => state.authors, (state) => state.authors,
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST), createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blocklist, authors, isClearingBlocklistExecuting) => { (blacklist, authors, isClearingBlacklistExecuting) => {
return { return {
isAuthorFetching: authors.isFetching, isAuthorFetching: authors.isFetching,
isAuthorPopulated: authors.isPopulated, isAuthorPopulated: authors.isPopulated,
isClearingBlocklistExecuting, isClearingBlacklistExecuting,
...blocklist ...blacklist
}; };
} }
); );
} }
const mapDispatchToProps = { const mapDispatchToProps = {
...blocklistActions, ...blacklistActions,
executeCommand executeCommand
}; };
class BlocklistConnector extends Component { class BlacklistConnector extends Component {
// //
// Lifecycle // Lifecycle
@@ -39,27 +39,27 @@ class BlocklistConnector extends Component {
componentDidMount() { componentDidMount() {
const { const {
useCurrentPage, useCurrentPage,
fetchBlocklist, fetchBlacklist,
gotoBlocklistFirstPage gotoBlacklistFirstPage
} = this.props; } = this.props;
registerPagePopulator(this.repopulate); registerPagePopulator(this.repopulate);
if (useCurrentPage) { if (useCurrentPage) {
fetchBlocklist(); fetchBlacklist();
} else { } else {
gotoBlocklistFirstPage(); gotoBlacklistFirstPage();
} }
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) { if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlocklistFirstPage(); this.props.gotoBlacklistFirstPage();
} }
} }
componentWillUnmount() { componentWillUnmount() {
this.props.clearBlocklist(); this.props.clearBlacklist();
unregisterPagePopulator(this.repopulate); unregisterPagePopulator(this.repopulate);
} }
@@ -67,49 +67,49 @@ class BlocklistConnector extends Component {
// Control // Control
repopulate = () => { repopulate = () => {
this.props.fetchBlocklist(); this.props.fetchBlacklist();
} }
// //
// Listeners // Listeners
onFirstPagePress = () => { onFirstPagePress = () => {
this.props.gotoBlocklistFirstPage(); this.props.gotoBlacklistFirstPage();
} }
onPreviousPagePress = () => { onPreviousPagePress = () => {
this.props.gotoBlocklistPreviousPage(); this.props.gotoBlacklistPreviousPage();
} }
onNextPagePress = () => { onNextPagePress = () => {
this.props.gotoBlocklistNextPage(); this.props.gotoBlacklistNextPage();
} }
onLastPagePress = () => { onLastPagePress = () => {
this.props.gotoBlocklistLastPage(); this.props.gotoBlacklistLastPage();
} }
onPageSelect = (page) => { onPageSelect = (page) => {
this.props.gotoBlocklistPage({ page }); this.props.gotoBlacklistPage({ page });
} }
onRemoveSelected = (ids) => { onRemoveSelected = (ids) => {
this.props.removeBlocklistItems({ ids }); this.props.removeBlacklistItems({ ids });
} }
onSortPress = (sortKey) => { onSortPress = (sortKey) => {
this.props.setBlocklistSort({ sortKey }); this.props.setBlacklistSort({ sortKey });
} }
onTableOptionChange = (payload) => { onTableOptionChange = (payload) => {
this.props.setBlocklistTableOption(payload); this.props.setBlacklistTableOption(payload);
if (payload.pageSize) { if (payload.pageSize) {
this.props.gotoBlocklistFirstPage(); this.props.gotoBlacklistFirstPage();
} }
} }
onClearBlocklistPress = () => { onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST }); this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
} }
// //
@@ -117,7 +117,7 @@ class BlocklistConnector extends Component {
render() { render() {
return ( return (
<Blocklist <Blacklist
onFirstPagePress={this.onFirstPagePress} onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress} onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress} onNextPagePress={this.onNextPagePress}
@@ -126,30 +126,30 @@ class BlocklistConnector extends Component {
onRemoveSelected={this.onRemoveSelected} onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress} onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange} onTableOptionChange={this.onTableOptionChange}
onClearBlocklistPress={this.onClearBlocklistPress} onClearBlacklistPress={this.onClearBlacklistPress}
{...this.props} {...this.props}
/> />
); );
} }
} }
BlocklistConnector.propTypes = { BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired, useCurrentPage: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired, isClearingBlacklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlocklist: PropTypes.func.isRequired, fetchBlacklist: PropTypes.func.isRequired,
gotoBlocklistFirstPage: PropTypes.func.isRequired, gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlocklistPreviousPage: PropTypes.func.isRequired, gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlocklistNextPage: PropTypes.func.isRequired, gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlocklistLastPage: PropTypes.func.isRequired, gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlocklistPage: PropTypes.func.isRequired, gotoBlacklistPage: PropTypes.func.isRequired,
removeBlocklistItems: PropTypes.func.isRequired, removeBlacklistItems: PropTypes.func.isRequired,
setBlocklistSort: PropTypes.func.isRequired, setBlacklistSort: PropTypes.func.isRequired,
setBlocklistTableOption: PropTypes.func.isRequired, setBlacklistTableOption: PropTypes.func.isRequired,
clearBlocklist: PropTypes.func.isRequired, clearBlacklist: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired executeCommand: PropTypes.func.isRequired
}; };
export default withCurrentPage( export default withCurrentPage(
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector) connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
); );
@@ -10,7 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
class BlocklistDetailsModal extends Component { class BlacklistDetailsModal extends Component {
// //
// Render // Render
@@ -78,7 +78,7 @@ class BlocklistDetailsModal extends Component {
} }
} }
BlocklistDetailsModal.propTypes = { BlacklistDetailsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
protocol: PropTypes.string.isRequired, protocol: PropTypes.string.isRequired,
@@ -87,4 +87,4 @@ BlocklistDetailsModal.propTypes = {
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
export default BlocklistDetailsModal; export default BlacklistDetailsModal;
@@ -9,10 +9,10 @@ import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import BlocklistDetailsModal from './BlocklistDetailsModal'; import BlacklistDetailsModal from './BlacklistDetailsModal';
import styles from './BlocklistRow.css'; import styles from './BlacklistRow.css';
class BlocklistRow extends Component { class BlacklistRow extends Component {
// //
// Lifecycle // Lifecycle
@@ -142,7 +142,7 @@ class BlocklistRow extends Component {
/> />
<IconButton <IconButton
title={translate('RemoveFromBlocklist')} title={translate('RemoveFromBlacklist')}
name={icons.REMOVE} name={icons.REMOVE}
kind={kinds.DANGER} kind={kinds.DANGER}
onPress={onRemovePress} onPress={onRemovePress}
@@ -155,7 +155,7 @@ class BlocklistRow extends Component {
}) })
} }
<BlocklistDetailsModal <BlacklistDetailsModal
isOpen={this.state.isDetailsModalOpen} isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle} sourceTitle={sourceTitle}
protocol={protocol} protocol={protocol}
@@ -169,7 +169,7 @@ class BlocklistRow extends Component {
} }
BlocklistRow.propTypes = { BlacklistRow.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
author: PropTypes.object.isRequired, author: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
@@ -184,4 +184,4 @@ BlocklistRow.propTypes = {
onRemovePress: PropTypes.func.isRequired onRemovePress: PropTypes.func.isRequired
}; };
export default BlocklistRow; export default BlacklistRow;
@@ -1,8 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { removeBlocklistItem } from 'Store/Actions/blocklistActions'; import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector'; import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import BlocklistRow from './BlocklistRow'; import BlacklistRow from './BlacklistRow';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
@@ -18,9 +18,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) { function createMapDispatchToProps(dispatch, props) {
return { return {
onRemovePress() { onRemovePress() {
dispatch(removeBlocklistItem({ id: props.id })); dispatch(removeBlacklistItem({ id: props.id }));
} }
}; };
} }
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow); export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
@@ -215,13 +215,13 @@ function HistoryDetails(props) {
switch (reason) { switch (reason) {
case 'Manual': case 'Manual':
reasonMessage = translate('FileWasDeletedByViaUI'); reasonMessage = 'File was deleted by via UI';
break; break;
case 'MissingFromDisk': case 'MissingFromDisk':
reasonMessage = translate('MissingFromDisk'); reasonMessage = 'Readarr was unable to find the file on disk so it was removed';
break; break;
case 'Upgrade': case 'Upgrade':
reasonMessage = translate('FileWasDeletedByUpgrade'); reasonMessage = 'File was deleted to import an upgrade';
break; break;
default: default:
reasonMessage = ''; reasonMessage = '';
@@ -23,6 +23,8 @@ function getIconName(eventType) {
return icons.RETAG; return icons.RETAG;
case 'bookImportIncomplete': case 'bookImportIncomplete':
return icons.DOWNLOADED; return icons.DOWNLOADED;
case 'downloadImported':
return icons.DOWNLOADED;
case 'downloadIgnored': case 'downloadIgnored':
return icons.IGNORE; return icons.IGNORE;
default: default:
@@ -59,6 +61,8 @@ function getTooltip(eventType, data) {
return 'Book file tags updated'; return 'Book file tags updated';
case 'bookImportIncomplete': case 'bookImportIncomplete':
return 'Files downloaded but not all could be imported'; return 'Files downloaded but not all could be imported';
case 'downloadImported':
return 'Download completed and successfully imported';
case 'downloadIgnored': case 'downloadIgnored':
return 'Book Download Ignored'; return 'Book Download Ignored';
default: default:
@@ -169,16 +169,6 @@ class HistoryRow extends Component {
); );
} }
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') { if (name === 'details') {
return ( return (
<TableRowCell <TableRowCell
+2 -13
View File
@@ -223,14 +223,14 @@ class Queue extends Component {
{ {
!isRefreshing && hasError && !isRefreshing && hasError &&
<div> <div>
{translate('FailedToLoadQueue')} Failed to load Queue
</div> </div>
} }
{ {
isAllPopulated && !hasError && !items.length && isAllPopulated && !hasError && !items.length &&
<div> <div>
{translate('QueueIsEmpty')} Queue is empty
</div> </div>
} }
@@ -284,17 +284,6 @@ class Queue extends Component {
return !!(item && item.authorId && item.bookId); 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} onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose} onModalClose={this.onConfirmRemoveModalClose}
/> />
+2 -3
View File
@@ -43,14 +43,14 @@ class QueueRow extends Component {
this.setState({ isRemoveQueueItemModalOpen: true }); this.setState({ isRemoveQueueItemModalOpen: true });
} }
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => { onRemoveQueueItemModalConfirmed = (blacklist, skipredownload) => {
const { const {
onRemoveQueueItemPress, onRemoveQueueItemPress,
onQueueRowModalOpenOrClose onQueueRowModalOpenOrClose
} = this.props; } = this.props;
onQueueRowModalOpenOrClose(false); onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist, skipredownload); onRemoveQueueItemPress(blacklist, skipredownload);
this.setState({ isRemoveQueueItemModalOpen: false }); this.setState({ isRemoveQueueItemModalOpen: false });
} }
@@ -357,7 +357,6 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen} isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title} sourceTitle={title}
canIgnore={!!author} canIgnore={!!author}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed} onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose} onModalClose={this.onRemoveQueueItemModalClose}
/> />
@@ -22,7 +22,7 @@ class RemoveQueueItemModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false, blacklist: false,
skipredownload: false skipredownload: false
}; };
} }
@@ -33,7 +33,7 @@ class RemoveQueueItemModal extends Component {
resetState = function() { resetState = function() {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false, blacklist: false,
skipredownload: false skipredownload: false
}); });
} }
@@ -45,8 +45,8 @@ class RemoveQueueItemModal extends Component {
this.setState({ remove: value }); this.setState({ remove: value });
} }
onBlocklistChange = ({ value }) => { onBlacklistChange = ({ value }) => {
this.setState({ blocklist: value }); this.setState({ blacklist: value });
} }
onSkipReDownloadChange = ({ value }) => { onSkipReDownloadChange = ({ value }) => {
@@ -72,11 +72,10 @@ class RemoveQueueItemModal extends Component {
const { const {
isOpen, isOpen,
sourceTitle, sourceTitle,
canIgnore, canIgnore
isPending
} = this.props; } = this.props;
const { remove, blocklist, skipredownload } = this.state; const { remove, blacklist, skipredownload } = this.state;
return ( return (
<Modal <Modal
@@ -96,41 +95,37 @@ class RemoveQueueItemModal extends Component {
Are you sure you want to remove '{sourceTitle}' from the queue? Are you sure you want to remove '{sourceTitle}' from the queue?
</div> </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> <FormGroup>
<FormLabel> <FormLabel>
{translate('BlocklistRelease')} {translate('RemoveFromDownloadClient')}
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="remove"
value={blocklist} value={remove}
helpText={translate('BlocklistHelpText')} helpTextWarning={translate('RemoveHelpTextWarning')}
onChange={this.onBlocklistChange} isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('BlacklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText={translate('BlacklistHelpText')}
onChange={this.onBlacklistChange}
/> />
</FormGroup> </FormGroup>
{ {
blocklist && blacklist &&
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('SkipRedownload')} {translate('SkipRedownload')}
@@ -169,7 +164,6 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired, canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired, onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -23,7 +23,7 @@ class RemoveQueueItemsModal extends Component {
this.state = { this.state = {
remove: true, remove: true,
blocklist: false, blacklist: false,
skipredownload: false skipredownload: false
}; };
} }
@@ -34,7 +34,7 @@ class RemoveQueueItemsModal extends Component {
resetState = function() { resetState = function() {
this.setState({ this.setState({
remove: true, remove: true,
blocklist: false, blacklist: false,
skipredownload: false skipredownload: false
}); });
} }
@@ -46,8 +46,8 @@ class RemoveQueueItemsModal extends Component {
this.setState({ remove: value }); this.setState({ remove: value });
} }
onBlocklistChange = ({ value }) => { onBlacklistChange = ({ value }) => {
this.setState({ blocklist: value }); this.setState({ blacklist: value });
} }
onSkipReDownloadChange = ({ value }) => { onSkipReDownloadChange = ({ value }) => {
@@ -73,11 +73,10 @@ class RemoveQueueItemsModal extends Component {
const { const {
isOpen, isOpen,
selectedCount, selectedCount,
canIgnore, canIgnore
allPending
} = this.props; } = this.props;
const { remove, blocklist, skipredownload } = this.state; const { remove, blacklist, skipredownload } = this.state;
return ( return (
<Modal <Modal
@@ -97,41 +96,37 @@ class RemoveQueueItemsModal extends Component {
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue? Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
</div> </div>
{
allPending ?
null :
<FormGroup>
<FormLabel>
{translate('RemoveFromDownloadClient')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist {translate('RemoveFromDownloadClient')}
</FormLabel> </FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.CHECK} type={inputTypes.CHECK}
name="blocklist" name="remove"
value={blocklist} value={remove}
helpText={translate('BlocklistHelpText')} helpTextWarning={translate('RemoveHelpTextWarning')}
onChange={this.onBlocklistChange} isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blacklist"
value={blacklist}
helpText={translate('BlacklistHelpText')}
onChange={this.onBlacklistChange}
/> />
</FormGroup> </FormGroup>
{ {
blocklist && blacklist &&
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
{translate('SkipRedownload')} {translate('SkipRedownload')}
@@ -170,7 +165,6 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired, isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired, selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired, canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired, onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -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;
@@ -1,52 +1,46 @@
import React from 'react'; import React from 'react';
import Alert from 'Components/Alert';
import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
function AuthorMonitoringOptionsPopoverContent() { function AuthorMonitoringOptionsPopoverContent() {
return ( return (
<> <DescriptionList>
<Alert> <DescriptionListItem
{translate('MonitoringOptionsHelpText')} title={translate('AllBooks')}
</Alert> data="Monitor all books"
<DescriptionList> />
<DescriptionListItem
title={translate('AllBooks')}
data={translate('DataAllBooks')}
/>
<DescriptionListItem <DescriptionListItem
title={translate('FutureBooks')} title={translate('FutureBooks')}
data={translate('DataFutureBooks')} data="Monitor books that have not released yet"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('MissingBooks')} title={translate('MissingBooks')}
data={translate('DataMissingBooks')} data="Monitor books that do not have files or have not released yet"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('ExistingBooks')} title={translate('ExistingBooks')}
data={translate('DataExistingBooks')} data="Monitor books that have files or have not released yet"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('FirstBook')} title={translate('FirstBook')}
data={translate('DataFirstBook')} data="Monitor the first book. All other books will be ignored"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('LatestBook')} title={translate('LatestBook')}
data={translate('DataLatestBook')} data="Monitor the latest book and future books"
/> />
<DescriptionListItem <DescriptionListItem
title={translate('None')} title={translate('None')}
data={translate('DataNone')} data="No books will be monitored"
/> />
</DescriptionList> </DescriptionList>
</>
); );
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) { function App({ store, history }) {
return ( return (
<DocumentTitle title={window.Readarr.instanceName}> <DocumentTitle title="Readarr">
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<PageConnector> <PageConnector>
+9 -3
View File
@@ -1,10 +1,11 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import { Redirect, Route } from 'react-router-dom'; 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 HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector'; import QueueConnector from 'Activity/Queue/QueueConnector';
import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector'; import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector';
import AuthorEditorConnector from 'Author/Editor/AuthorEditorConnector';
import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector'; import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector';
import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector'; import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector';
import BookIndexConnector from 'Book/Index/BookIndexConnector'; import BookIndexConnector from 'Book/Index/BookIndexConnector';
@@ -81,6 +82,11 @@ function AppRoutes(props) {
component={AddNewItemConnector} component={AddNewItemConnector}
/> />
<Route
path="/authoreditor"
component={AuthorEditorConnector}
/>
<Route <Route
exact={true} exact={true}
path="/shelf" path="/shelf"
@@ -132,8 +138,8 @@ function AppRoutes(props) {
/> />
<Route <Route
path="/activity/blocklist" path="/activity/blacklist"
component={BlocklistConnector} component={BlacklistConnector}
/> />
{/* {/*
+1 -3
View File
@@ -39,9 +39,7 @@ function AppUpdatedModalContent(props) {
<div> <div>
{ {
!update.changes && !update.changes &&
<div className={styles.maintenance}> <div className={styles.maintenance}>Maintenance release</div>
{translate('MaintenanceRelease')}
</div>
} }
{ {
+6 -131
View File
@@ -5,7 +5,6 @@ import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector'; import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorHistoryTable from 'Author/History/AuthorHistoryTable'; import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal'; import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable'; import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
@@ -23,7 +22,6 @@ import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector'; import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll'; import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected'; import toggleSelected from 'Utilities/Table/toggleSelected';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal'; import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
@@ -55,56 +53,13 @@ class AuthorDetails extends Component {
isDeleteAuthorModalOpen: false, isDeleteAuthorModalOpen: false,
isInteractiveImportModalOpen: false, isInteractiveImportModalOpen: false,
isMonitorOptionsModalOpen: false, isMonitorOptionsModalOpen: false,
isEditorActive: false,
allExpanded: false, allExpanded: false,
allCollapsed: false, allCollapsed: false,
expandedState: {}, expandedState: {},
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {},
selectedTabIndex: 0 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 // Listeners
@@ -159,10 +114,6 @@ class AuthorDetails extends Component {
this.setState({ isMonitorOptionsModalOpen: false }); this.setState({ isMonitorOptionsModalOpen: false });
} }
onBookEditorTogglePress = () => {
this.setState({ isEditorActive: !this.state.isEditorActive });
}
onExpandAllPress = () => { onExpandAllPress = () => {
const { const {
allExpanded, allExpanded,
@@ -186,27 +137,6 @@ class AuthorDetails extends Component {
}); });
} }
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) => { onTabSelect = (index, lastIndex) => {
this.setState({ selectedTabIndex: index }); this.setState({ selectedTabIndex: index });
} }
@@ -235,10 +165,6 @@ class AuthorDetails extends Component {
nextAuthor, nextAuthor,
onRefreshPress, onRefreshPress,
onSearchPress, onSearchPress,
isSaving,
saveError,
isDeleting,
deleteError,
statistics statistics
} = this.props; } = this.props;
@@ -249,9 +175,6 @@ class AuthorDetails extends Component {
isDeleteAuthorModalOpen, isDeleteAuthorModalOpen,
isInteractiveImportModalOpen, isInteractiveImportModalOpen,
isMonitorOptionsModalOpen, isMonitorOptionsModalOpen,
isEditorActive,
allSelected,
selectedState,
allExpanded, allExpanded,
allCollapsed, allCollapsed,
expandedState, expandedState,
@@ -266,14 +189,12 @@ class AuthorDetails extends Component {
expandIcon = icons.EXPAND; expandIcon = icons.EXPAND;
} }
const selectedBookIds = this.getSelectedIds();
return ( return (
<PageContent title={authorName}> <PageContent title={authorName}>
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
label={translate('RefreshAndScan')} label={translate('RefreshScan')}
iconName={icons.REFRESH} iconName={icons.REFRESH}
spinningName={icons.REFRESH} spinningName={icons.REFRESH}
title={translate('RefreshInformationAndScanDisk')} title={translate('RefreshInformationAndScanDisk')}
@@ -331,33 +252,6 @@ class AuthorDetails extends Component {
iconName={icons.DELETE} iconName={icons.DELETE}
onPress={this.onDeleteAuthorPress} 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>
<PageToolbarSection alignContent={align.RIGHT}> <PageToolbarSection alignContent={align.RIGHT}>
@@ -483,11 +377,7 @@ class AuthorDetails extends Component {
<AuthorDetailsSeasonConnector <AuthorDetailsSeasonConnector
authorId={id} authorId={id}
isExpanded={true} isExpanded={true}
selectedState={selectedState}
onExpandPress={this.onExpandPress} onExpandPress={this.onExpandPress}
setSelectedState={this.setSelectedState}
onSelectedChange={this.onSelectedChange}
isEditorActive={isEditorActive}
/> />
</TabPanel> </TabPanel>
@@ -532,6 +422,7 @@ class AuthorDetails extends Component {
</TabPanel> </TabPanel>
</Tabs> </Tabs>
} }
</div> </div>
<div className={styles.metadataMessage}> <div className={styles.metadataMessage}>
@@ -583,19 +474,6 @@ class AuthorDetails extends Component {
onModalClose={this.onMonitorOptionsClose} onModalClose={this.onMonitorOptionsClose}
/> />
</PageContentBody> </PageContentBody>
{
isEditorActive &&
<BookEditorFooter
bookIds={selectedBookIds}
selectedCount={selectedBookIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
onSaveSelected={this.onSaveSelected}
/>
}
</PageContent> </PageContent>
); );
} }
@@ -615,6 +493,7 @@ AuthorDetails.propTypes = {
images: PropTypes.arrayOf(PropTypes.object).isRequired, images: PropTypes.arrayOf(PropTypes.object).isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired, alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired, tags: PropTypes.arrayOf(PropTypes.number).isRequired,
isSaving: PropTypes.bool.isRequired,
isRefreshing: PropTypes.bool.isRequired, isRefreshing: PropTypes.bool.isRequired,
isSearching: PropTypes.bool.isRequired, isSearching: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired, isFetching: PropTypes.bool.isRequired,
@@ -631,17 +510,13 @@ AuthorDetails.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
onMonitorTogglePress: PropTypes.func.isRequired, onMonitorTogglePress: PropTypes.func.isRequired,
onRefreshPress: PropTypes.func.isRequired, onRefreshPress: PropTypes.func.isRequired,
onSearchPress: 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
}; };
AuthorDetails.defaultProps = { AuthorDetails.defaultProps = {
statistics: {}, statistics: {},
tags: [] tags: [],
isSaving: false
}; };
export default AuthorDetails; export default AuthorDetails;
@@ -7,7 +7,6 @@ import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import { toggleAuthorMonitored } from 'Store/Actions/authorActions'; import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions'; import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { saveBookEditor } from 'Store/Actions/bookIndexActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions'; import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
@@ -22,8 +21,7 @@ import AuthorDetails from './AuthorDetails';
const selectBooks = createSelector( const selectBooks = createSelector(
(state) => state.books, (state) => state.books,
(state) => state.bookIndex, (books) => {
(books, index) => {
const { const {
items, items,
isFetching, isFetching,
@@ -31,13 +29,6 @@ const selectBooks = createSelector(
error error
} = books; } = books;
const {
isSaving,
saveError,
isDeleting,
deleteError
} = index;
const hasBooks = !!items.length; const hasBooks = !!items.length;
const hasMonitoredBooks = items.some((e) => e.monitored); const hasMonitoredBooks = items.some((e) => e.monitored);
@@ -46,11 +37,7 @@ const selectBooks = createSelector(
isBooksPopulated: isPopulated, isBooksPopulated: isPopulated,
booksError: error, booksError: error,
hasBooks, hasBooks,
hasMonitoredBooks, hasMonitoredBooks
isSaving,
saveError,
isDeleting,
deleteError
}; };
} }
); );
@@ -122,11 +109,7 @@ function createMapStateToProps() {
isBooksPopulated, isBooksPopulated,
booksError, booksError,
hasBooks, hasBooks,
hasMonitoredBooks, hasMonitoredBooks
isSaving,
saveError,
isDeleting,
deleteError
} = books; } = books;
const { const {
@@ -186,10 +169,6 @@ function createMapStateToProps() {
isFetching, isFetching,
isPopulated, isPopulated,
booksError, booksError,
isSaving,
saveError,
isDeleting,
deleteError,
seriesError, seriesError,
bookFilesError, bookFilesError,
hasBooks, hasBooks,
@@ -208,7 +187,6 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
fetchSeries, fetchSeries,
clearSeries, clearSeries,
saveBookEditor,
fetchBookFiles, fetchBookFiles,
clearBookFiles, clearBookFiles,
toggleAuthorMonitored, toggleAuthorMonitored,
@@ -304,10 +282,6 @@ class AuthorDetailsConnector extends Component {
}); });
} }
onSaveSelected = (payload) => {
this.props.saveBookEditor(payload);
}
// //
// Render // Render
@@ -318,7 +292,6 @@ class AuthorDetailsConnector extends Component {
onMonitorTogglePress={this.onMonitorTogglePress} onMonitorTogglePress={this.onMonitorTogglePress}
onRefreshPress={this.onRefreshPress} onRefreshPress={this.onRefreshPress}
onSearchPress={this.onSearchPress} onSearchPress={this.onSearchPress}
onSaveSelected={this.onSaveSelected}
/> />
); );
} }
@@ -334,7 +307,6 @@ AuthorDetailsConnector.propTypes = {
isRenamingAuthor: PropTypes.bool.isRequired, isRenamingAuthor: PropTypes.bool.isRequired,
fetchSeries: PropTypes.func.isRequired, fetchSeries: PropTypes.func.isRequired,
clearSeries: PropTypes.func.isRequired, clearSeries: PropTypes.func.isRequired,
saveBookEditor: PropTypes.func.isRequired,
fetchBookFiles: PropTypes.func.isRequired, fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired, clearBookFiles: PropTypes.func.isRequired,
toggleAuthorMonitored: PropTypes.func.isRequired, toggleAuthorMonitored: PropTypes.func.isRequired,
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
import Table from 'Components/Table/Table'; import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody'; import TableBody from 'Components/Table/TableBody';
import { sortDirections } from 'Helpers/Props'; import { sortDirections } from 'Helpers/Props';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import getToggledRange from 'Utilities/Table/getToggledRange'; import getToggledRange from 'Utilities/Table/getToggledRange';
import BookRowConnector from './BookRowConnector'; import BookRowConnector from './BookRowConnector';
import styles from './AuthorDetailsSeason.css'; 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 // Listeners
@@ -63,42 +42,26 @@ class AuthorDetailsSeason extends Component {
this.props.onMonitorBookPress(_.uniq(bookIds), monitored); this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
} }
onSelectedChange = ({ id, value, shiftKey = false }) => {
const {
onSelectedChange,
items
} = this.props;
return onSelectedChange(items, id, value, shiftKey);
}
// //
// Render // Render
render() { render() {
const { const {
items, items,
isEditorActive,
columns, columns,
sortKey, sortKey,
sortDirection, sortDirection,
onSortPress, onSortPress,
onTableOptionChange, onTableOptionChange
selectedState
} = this.props; } = this.props;
let titleColumns = columns;
if (!isEditorActive) {
titleColumns = columns.filter((x) => x.name !== 'select');
}
return ( return (
<div <div
className={styles.bookType} className={styles.bookType}
> >
<div className={styles.books}> <div className={styles.books}>
<Table <Table
columns={titleColumns} columns={columns}
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onSortPress={onSortPress} onSortPress={onSortPress}
@@ -113,9 +76,6 @@ class AuthorDetailsSeason extends Component {
columns={columns} columns={columns}
{...item} {...item}
onMonitorBookPress={this.onMonitorBookPress} onMonitorBookPress={this.onMonitorBookPress}
isEditorActive={isEditorActive}
isSelected={selectedState[item.id]}
onSelectedChange={this.onSelectedChange}
/> />
); );
}) })
@@ -132,13 +92,9 @@ AuthorDetailsSeason.propTypes = {
sortKey: PropTypes.string, sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
items: PropTypes.arrayOf(PropTypes.object).isRequired, items: PropTypes.arrayOf(PropTypes.object).isRequired,
isEditorActive: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired, onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired, onExpandPress: PropTypes.func.isRequired,
setSelectedState: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired,
onMonitorBookPress: PropTypes.func.isRequired, onMonitorBookPress: PropTypes.func.isRequired,
uiSettings: PropTypes.object.isRequired uiSettings: PropTypes.object.isRequired
-19
View File
@@ -6,7 +6,6 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import StarRating from 'Components/StarRating'; import StarRating from 'Components/StarRating';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import BookStatus from './BookStatus'; import BookStatus from './BookStatus';
import styles from './BookRow.css'; import styles from './BookRow.css';
@@ -66,9 +65,6 @@ class BookRow extends Component {
authorMonitored, authorMonitored,
titleSlug, titleSlug,
bookFiles, bookFiles,
isEditorActive,
isSelected,
onSelectedChange,
columns columns
} = this.props; } = this.props;
@@ -88,18 +84,6 @@ class BookRow extends Component {
return null; return null;
} }
if (isEditorActive && name === 'select') {
return (
<TableSelectCell
key={name}
id={id}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'monitored') { if (name === 'monitored') {
return ( return (
<TableRowCell <TableRowCell
@@ -236,9 +220,6 @@ BookRow.propTypes = {
isSaving: PropTypes.bool, isSaving: PropTypes.bool,
authorMonitored: PropTypes.bool.isRequired, authorMonitored: PropTypes.bool.isRequired,
bookFiles: PropTypes.arrayOf(PropTypes.object).isRequired, bookFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onMonitorBookPress: PropTypes.func.isRequired onMonitorBookPress: PropTypes.func.isRequired
}; };
@@ -1,7 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent'; import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal'; import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
@@ -74,7 +73,6 @@ class EditAuthorModalContent extends Component {
const { const {
monitored, monitored,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
path, path,
@@ -103,31 +101,6 @@ class EditAuthorModalContent extends Component {
/> />
</FormGroup> </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> <FormGroup>
<FormLabel> <FormLabel>
{translate('QualityProfile')} {translate('QualityProfile')}
@@ -39,7 +39,6 @@ function createMapStateToProps() {
const authorSettings = _.pick(author, [ const authorSettings = _.pick(author, [
'monitored', 'monitored',
'monitorNewItems',
'qualityProfileId', 'qualityProfileId',
'metadataProfileId', 'metadataProfileId',
'path', 'path',
+280
View File
@@ -0,0 +1,280 @@
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 translate from 'Utilities/String/translate';
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={translate('AuthorEditor')}>
<PageToolbar>
<PageToolbarSection />
<PageToolbarSection alignContent={align.RIGHT}>
<TableOptionsModalWrapper
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<PageToolbarButton
label={translate('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;
@@ -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);
@@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setAuthorEditorFilter } from 'Store/Actions/authorEditorActions';
function createMapStateToProps() {
return createSelector(
(state) => state.authors.items,
(state) => state.authorEditor.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'authorEditor'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setAuthorEditorFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
@@ -1,23 +1,11 @@
.footer {
display: flex;
flex-direction: column;
flex-grow: 1;
}
.dropdownContainer {
display: flex;
flex-wrap: wrap;
margin-bottom: 10px;
}
.inputContainer { .inputContainer {
flex: 1;
margin-right: 20px; margin-right: 20px;
min-width: 150px; min-width: 150px;
} }
.buttonContainer { .buttonContainer {
display: flex; display: flex;
justify-content: flex-end;
flex-grow: 1; flex-grow: 1;
} }
@@ -36,14 +24,12 @@
composes: button from '~Components/Link/SpinnerButton.css'; composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px; margin-right: 10px;
margin-bottom: 10px;
height: 35px; height: 35px;
} }
.deleteSelectedButton { .deleteSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css'; composes: button from '~Components/Link/SpinnerButton.css';
margin-bottom: 10px;
margin-left: 50px; margin-left: 50px;
height: 35px; height: 35px;
} }
@@ -62,10 +48,6 @@
} }
@media only screen and (max-width: $breakpointSmall) { @media only screen and (max-width: $breakpointSmall) {
.dropdownContainer {
display: block;
}
.inputContainer { .inputContainer {
margin-right: 0; margin-right: 0;
} }
@@ -79,7 +61,6 @@
} }
.buttons { .buttons {
display: block;
justify-content: space-between; justify-content: space-between;
} }
+109 -99
View File
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal'; import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector'; import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector'; import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
@@ -27,7 +26,6 @@ class AuthorEditorFooter extends Component {
this.state = { this.state = {
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE, qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE, metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE, rootFolderPath: NO_CHANGE,
@@ -48,7 +46,6 @@ class AuthorEditorFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitorNewItems: NO_CHANGE,
qualityProfileId: NO_CHANGE, qualityProfileId: NO_CHANGE,
metadataProfileId: NO_CHANGE, metadataProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE, rootFolderPath: NO_CHANGE,
@@ -142,13 +139,13 @@ class AuthorEditorFooter extends Component {
isDeleting, isDeleting,
isOrganizingAuthor, isOrganizingAuthor,
isRetaggingAuthor, isRetaggingAuthor,
columns,
onOrganizeAuthorPress, onOrganizeAuthorPress,
onRetagAuthorPress onRetagAuthorPress
} = this.props; } = this.props;
const { const {
monitored, monitored,
monitorNewItems,
qualityProfileId, qualityProfileId,
metadataProfileId, metadataProfileId,
rootFolderPath, rootFolderPath,
@@ -167,99 +164,112 @@ class AuthorEditorFooter extends Component {
return ( return (
<PageContentFooter> <PageContentFooter>
<div className={styles.footer}> <div className={styles.inputContainer}>
<div className={styles.dropdownContainer}> <AuthorEditorFooterLabel
<div className={styles.inputContainer}> label={translate('MonitorAuthor')}
<AuthorEditorFooterLabel isSaving={isSaving && monitored !== NO_CHANGE}
label={translate('MonitorAuthor')} />
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput <SelectInput
name="monitored" name="monitored"
value={monitored} value={monitored}
values={monitoredOptions} values={monitoredOptions}
isDisabled={!selectedCount} isDisabled={!selectedCount}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
</div> </div>
<div className={styles.inputContainer}> {
<AuthorEditorFooterLabel columns.map((column) => {
label={translate('MonitorNewItems')} const {
isSaving={isSaving && monitored !== NO_CHANGE} name,
/> isVisible
} = column;
<MonitorNewItemsSelectInput if (!isVisible) {
name="monitorNewItems" return null;
value={monitorNewItems} }
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}> if (name === 'qualityProfileId') {
<AuthorEditorFooterLabel return (
label={translate('QualityProfile')} <div
isSaving={isSaving && qualityProfileId !== NO_CHANGE} key={name}
/> className={styles.inputContainer}
>
<AuthorEditorFooterLabel
label={translate('QualityProfile')}
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector <QualityProfileSelectInputConnector
name="qualityProfileId" name="qualityProfileId"
value={qualityProfileId} value={qualityProfileId}
includeNoChange={true} includeNoChange={true}
isDisabled={!selectedCount} isDisabled={!selectedCount}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
</div> </div>
);
}
<div if (name === 'metadataProfileId') {
className={styles.inputContainer} return (
> <div
<AuthorEditorFooterLabel key={name}
label={translate('MetadataProfile')} className={styles.inputContainer}
isSaving={isSaving && metadataProfileId !== NO_CHANGE} >
/> <AuthorEditorFooterLabel
label={translate('MetadataProfile')}
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
/>
<MetadataProfileSelectInputConnector <MetadataProfileSelectInputConnector
name="metadataProfileId" name="metadataProfileId"
value={metadataProfileId} value={metadataProfileId}
includeNoChange={true} includeNoChange={true}
includeNone={true} isDisabled={!selectedCount}
isDisabled={!selectedCount} onChange={this.onInputChange}
onChange={this.onInputChange} />
/> </div>
</div> );
}
<div if (name === 'path') {
className={styles.inputContainer} return (
> <div
<AuthorEditorFooterLabel key={name}
label={translate('RootFolder')} className={styles.inputContainer}
isSaving={isSaving && rootFolderPath !== NO_CHANGE} >
/> <AuthorEditorFooterLabel
label={translate('RootFolder')}
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
/>
<RootFolderSelectInputConnector <RootFolderSelectInputConnector
name="rootFolderPath" name="rootFolderPath"
value={rootFolderPath} value={rootFolderPath}
includeNoChange={true} includeNoChange={true}
isDisabled={!selectedCount} isDisabled={!selectedCount}
selectedValueOptions={{ includeFreeSpace: false }} selectedValueOptions={{ includeFreeSpace: false }}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
</div> </div>
</div> );
}
<div className={styles.buttonContainer}> return null;
<div className={styles.buttonContainerContent}> })
<AuthorEditorFooterLabel }
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}> <div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<AuthorEditorFooterLabel
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton <SpinnerButton
className={styles.organizeSelectedButton} className={styles.organizeSelectedButton}
kind={kinds.WARNING} kind={kinds.WARNING}
@@ -267,7 +277,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onOrganizeAuthorPress} onPress={onOrganizeAuthorPress}
> >
{translate('RenameFiles')} Rename Files
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -277,7 +287,7 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={onRetagAuthorPress} onPress={onRetagAuthorPress}
> >
{translate('WriteMetadataTags')} Write Metadata Tags
</SpinnerButton> </SpinnerButton>
<SpinnerButton <SpinnerButton
@@ -286,20 +296,19 @@ class AuthorEditorFooter extends Component {
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor} isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
onPress={this.onTagsPress} onPress={this.onTagsPress}
> >
{translate('SetReadarrTags')} Set Readarr Tags
</SpinnerButton> </SpinnerButton>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
{translate('Delete')}
</SpinnerButton>
</div> </div>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton>
</div> </div>
</div> </div>
</div> </div>
@@ -339,6 +348,7 @@ AuthorEditorFooter.propTypes = {
isOrganizingAuthor: PropTypes.bool.isRequired, isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired, isRetaggingAuthor: PropTypes.bool.isRequired,
showMetadataProfile: PropTypes.bool.isRequired, showMetadataProfile: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onSaveSelected: PropTypes.func.isRequired, onSaveSelected: PropTypes.func.isRequired,
onOrganizeAuthorPress: PropTypes.func.isRequired, onOrganizeAuthorPress: PropTypes.func.isRequired,
onRetagAuthorPress: PropTypes.func.isRequired onRetagAuthorPress: PropTypes.func.isRequired
@@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { bulkDeleteAuthor } from 'Store/Actions/authorIndexActions'; import { bulkDeleteAuthor } from 'Store/Actions/authorEditorActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector'; import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import DeleteAuthorModalContent from './DeleteAuthorModalContent'; import DeleteAuthorModalContent from './DeleteAuthorModalContent';
+6 -210
View File
@@ -1,9 +1,6 @@
import _ from 'lodash'; import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import RetagAuthorModal from 'Author/Editor/AudioTags/RetagAuthorModal';
import AuthorEditorFooter from 'Author/Editor/AuthorEditorFooter';
import OrganizeAuthorModal from 'Author/Editor/Organize/OrganizeAuthorModal';
import NoAuthor from 'Author/NoAuthor'; import NoAuthor from 'Author/NoAuthor';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
@@ -18,9 +15,6 @@ import { align, icons, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import AuthorIndexFooterConnector from './AuthorIndexFooterConnector'; import AuthorIndexFooterConnector from './AuthorIndexFooterConnector';
import AuthorIndexFilterMenu from './Menus/AuthorIndexFilterMenu'; import AuthorIndexFilterMenu from './Menus/AuthorIndexFilterMenu';
import AuthorIndexSortMenu from './Menus/AuthorIndexSortMenu'; import AuthorIndexSortMenu from './Menus/AuthorIndexSortMenu';
@@ -58,20 +52,12 @@ class AuthorIndex extends Component {
jumpBarItems: { order: [] }, jumpBarItems: { order: [] },
jumpToCharacter: null, jumpToCharacter: null,
isPosterOptionsModalOpen: false, isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false, isOverviewOptionsModalOpen: false
isEditorActive: false,
isOrganizingAuthorModalOpen: false,
isRetaggingAuthorModalOpen: false,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
}; };
} }
componentDidMount() { componentDidMount() {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -86,7 +72,6 @@ class AuthorIndex extends Component {
hasDifferentItemsOrOrder(prevProps.items, items) hasDifferentItemsOrOrder(prevProps.items, items)
) { ) {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
if (this.state.jumpToCharacter != null) { if (this.state.jumpToCharacter != null) {
@@ -101,48 +86,6 @@ class AuthorIndex extends Component {
this.setState({ scroller: ref }); this.setState({ scroller: ref });
} }
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
}
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((author) => {
const isItemSelected = selectedState[author.id];
if (isItemSelected) {
newSelectedState[author.id] = isItemSelected;
} else {
newSelectedState[author.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 });
}
setJumpBarItems() { setJumpBarItems() {
const { const {
items, items,
@@ -206,72 +149,10 @@ class AuthorIndex extends Component {
this.setState({ isOverviewOptionsModalOpen: false }); this.setState({ isOverviewOptionsModalOpen: false });
} }
onEditorTogglePress = () => {
if (this.state.isEditorActive) {
this.setState({ isEditorActive: false });
} else {
const newState = selectAll(this.state.selectedState, false);
newState.isEditorActive = true;
this.setState(newState);
}
}
onJumpBarItemPress = (jumpToCharacter) => { onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter }); this.setState({ jumpToCharacter });
} }
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
}
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 });
}
}
onRefreshAuthorPress = () => {
const selectedIds = this.getSelectedIds();
const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : [];
this.props.onRefreshAuthorPress(refreshIds);
}
// //
// Render // Render
@@ -291,16 +172,11 @@ class AuthorIndex extends Component {
view, view,
isRefreshingAuthor, isRefreshingAuthor,
isRssSyncExecuting, isRssSyncExecuting,
isOrganizingAuthor,
isRetaggingAuthor,
isSaving,
saveError,
isDeleting,
deleteError,
onScroll, onScroll,
onSortSelect, onSortSelect,
onFilterSelect, onFilterSelect,
onViewSelect, onViewSelect,
onRefreshAuthorPress,
onRssSyncPress, onRssSyncPress,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -310,31 +186,23 @@ class AuthorIndex extends Component {
jumpBarItems, jumpBarItems,
jumpToCharacter, jumpToCharacter,
isPosterOptionsModalOpen, isPosterOptionsModalOpen,
isOverviewOptionsModalOpen, isOverviewOptionsModalOpen
isEditorActive,
selectedState,
allSelected,
allUnselected
} = this.state; } = this.state;
const selectedAuthorIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view); const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller); const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoAuthor = !totalItems; const hasNoAuthor = !totalItems;
const refreshLabel = isEditorActive && selectedAuthorIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
return ( return (
<PageContent> <PageContent>
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
label={refreshLabel} label={translate('UpdateAll')}
iconName={icons.REFRESH} iconName={icons.REFRESH}
spinningName={icons.REFRESH} spinningName={icons.REFRESH}
isSpinning={isRefreshingAuthor} isSpinning={isRefreshingAuthor}
onPress={this.onRefreshAuthorPress} onPress={onRefreshAuthorPress}
/> />
<PageToolbarButton <PageToolbarButton
@@ -345,35 +213,6 @@ class AuthorIndex extends Component {
onPress={onRssSyncPress} onPress={onRssSyncPress}
/> />
<PageToolbarSeparator />
{
isEditorActive ?
<PageToolbarButton
label={translate('AuthorIndex')}
iconName={icons.AUTHOR_CONTINUING}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/> :
<PageToolbarButton
label={translate('AuthorEditor')}
iconName={icons.EDIT}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/>
}
{
isEditorActive ?
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoAuthor}
onPress={this.onSelectAllPress}
/> :
null
}
</PageToolbarSection> </PageToolbarSection>
<PageToolbarSection <PageToolbarSection
@@ -471,12 +310,6 @@ class AuthorIndex extends Component {
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter} jumpToCharacter={jumpToCharacter}
isEditorActive={isEditorActive}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps} {...otherProps}
/> />
@@ -499,24 +332,6 @@ class AuthorIndex extends Component {
} }
</div> </div>
{
isLoaded && isEditorActive &&
<AuthorEditorFooter
authorIds={selectedAuthorIds}
selectedCount={selectedAuthorIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
isOrganizingAuthor={isOrganizingAuthor}
isRetaggingAuthor={isRetaggingAuthor}
showMetadataProfile={true}
onSaveSelected={this.onSaveSelected}
onOrganizeAuthorPress={this.onOrganizeAuthorPress}
onRetagAuthorPress={this.onRetagAuthorPress}
/>
}
<AuthorIndexPosterOptionsModal <AuthorIndexPosterOptionsModal
isOpen={isPosterOptionsModalOpen} isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose} onModalClose={this.onPosterOptionsModalClose}
@@ -525,20 +340,8 @@ class AuthorIndex extends Component {
<AuthorIndexOverviewOptionsModal <AuthorIndexOverviewOptionsModal
isOpen={isOverviewOptionsModalOpen} isOpen={isOverviewOptionsModalOpen}
onModalClose={this.onOverviewOptionsModalClose} onModalClose={this.onOverviewOptionsModalClose}
/>
<OrganizeAuthorModal
isOpen={this.state.isOrganizingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onOrganizeAuthorModalClose}
/> />
<RetagAuthorModal
isOpen={this.state.isRetaggingAuthorModalOpen}
authorIds={selectedAuthorIds}
onModalClose={this.onRetagAuthorModalClose}
/>
</PageContent> </PageContent>
); );
} }
@@ -558,21 +361,14 @@ AuthorIndex.propTypes = {
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
isRefreshingAuthor: PropTypes.bool.isRequired, isRefreshingAuthor: PropTypes.bool.isRequired,
isOrganizingAuthor: PropTypes.bool.isRequired,
isRetaggingAuthor: PropTypes.bool.isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired, isRssSyncExecuting: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired, onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired, onViewSelect: PropTypes.func.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired, onRefreshAuthorPress: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired, onRssSyncPress: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired, onScroll: PropTypes.func.isRequired
onSaveSelected: PropTypes.func.isRequired
}; };
export default AuthorIndex; export default AuthorIndex;
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition'; import withScrollPosition from 'Components/withScrollPosition';
import { saveAuthorEditor, setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions'; import { setAuthorFilter, setAuthorSort, setAuthorTableOption, setAuthorView } from 'Store/Actions/authorIndexActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector'; import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
@@ -18,22 +18,16 @@ function createMapStateToProps() {
createAuthorClientSideCollectionItemsSelector('authorIndex'), createAuthorClientSideCollectionItemsSelector('authorIndex'),
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
createDimensionsSelector(), createDimensionsSelector(),
( (
author, author,
isRefreshingAuthor, isRefreshingAuthor,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting, isRssSyncExecuting,
dimensionsState dimensionsState
) => { ) => {
return { return {
...author, ...author,
isRefreshingAuthor, isRefreshingAuthor,
isOrganizingAuthor,
isRetaggingAuthor,
isRssSyncExecuting, isRssSyncExecuting,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
@@ -59,14 +53,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setAuthorView({ view })); dispatch(setAuthorView({ view }));
}, },
dispatchSaveAuthorEditor(payload) { onRefreshAuthorPress() {
dispatch(saveAuthorEditor(payload));
},
onRefreshAuthorPress(items) {
dispatch(executeCommand({ dispatch(executeCommand({
name: commandNames.BULK_REFRESH_AUTHOR, name: commandNames.REFRESH_AUTHOR
authorIds: items
})); }));
}, },
@@ -87,10 +76,6 @@ class AuthorIndexConnector extends Component {
this.props.dispatchSetAuthorView(view); this.props.dispatchSetAuthorView(view);
} }
onSaveSelected = (payload) => {
this.props.dispatchSaveAuthorEditor(payload);
}
onScroll = ({ scrollTop }) => { onScroll = ({ scrollTop }) => {
scrollPositions.authorIndex = scrollTop; scrollPositions.authorIndex = scrollTop;
} }
@@ -104,7 +89,6 @@ class AuthorIndexConnector extends Component {
{...this.props} {...this.props}
onViewSelect={this.onViewSelect} onViewSelect={this.onViewSelect}
onScroll={this.onScroll} onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
/> />
); );
} }
@@ -113,8 +97,7 @@ class AuthorIndexConnector extends Component {
AuthorIndexConnector.propTypes = { AuthorIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
dispatchSetAuthorView: PropTypes.func.isRequired, dispatchSetAuthorView: PropTypes.func.isRequired
dispatchSaveAuthorEditor: PropTypes.func.isRequired
}; };
export default withScrollPosition( export default withScrollPosition(
@@ -19,13 +19,6 @@ $hoverScale: 1.05;
left: 0; left: 0;
} }
.editorSelect {
position: absolute;
top: 0;
left: 5px;
z-index: 3;
}
.posterContainer { .posterContainer {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -5,7 +5,6 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal'; import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector'; import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar'; import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
@@ -68,15 +67,6 @@ class AuthorIndexOverview extends Component {
this.setState({ isDeleteAuthorModalOpen: false }); this.setState({ isDeleteAuthorModalOpen: false });
} }
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
// //
// Render // Render
@@ -107,8 +97,6 @@ class AuthorIndexOverview extends Component {
isSearchingAuthor, isSearchingAuthor,
onRefreshAuthorPress, onRefreshAuthorPress,
onSearchPress, onSearchPress,
isEditorActive,
isSelected,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -139,18 +127,6 @@ class AuthorIndexOverview extends Component {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
{ {
status === 'ended' && status === 'ended' &&
<div <div
@@ -294,10 +270,7 @@ AuthorIndexOverview.propTypes = {
isRefreshingAuthor: PropTypes.bool.isRequired, isRefreshingAuthor: PropTypes.bool.isRequired,
isSearchingAuthor: PropTypes.bool.isRequired, isSearchingAuthor: PropTypes.bool.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired, onRefreshAuthorPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
}; };
AuthorIndexOverview.defaultProps = { AuthorIndexOverview.defaultProps = {
@@ -73,9 +73,7 @@ class AuthorIndexOverviews extends Component {
sortKey, sortKey,
overviewOptions, overviewOptions,
jumpToCharacter, jumpToCharacter,
scrollTop, scrollTop
isEditorActive,
selectedState
} = this.props; } = this.props;
const { const {
@@ -93,8 +91,6 @@ class AuthorIndexOverviews extends Component {
(prevState.width !== width || (prevState.width !== width ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) || hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState ||
prevProps.overviewOptions.showTitle !== overviewOptions.showTitle)) { prevProps.overviewOptions.showTitle !== overviewOptions.showTitle)) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
@@ -152,10 +148,7 @@ class AuthorIndexOverviews extends Component {
shortDateFormat, shortDateFormat,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
isSmallScreen, isSmallScreen
selectedState,
isEditorActive,
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -191,9 +184,6 @@ class AuthorIndexOverviews extends Component {
authorId={author.id} authorId={author.id}
qualityProfileId={author.qualityProfileId} qualityProfileId={author.qualityProfileId}
metadataProfileId={author.metadataProfileId} metadataProfileId={author.metadataProfileId}
isSelected={selectedState[author.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/> />
</div> </div>
); );
@@ -274,10 +264,7 @@ AuthorIndexOverviews.propTypes = {
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
}; };
export default AuthorIndexOverviews; export default AuthorIndexOverviews;
@@ -78,13 +78,6 @@ $hoverScale: 1.05;
color: $white; color: $white;
} }
.editorSelect {
position: absolute;
top: 10px;
left: 10px;
z-index: 4;
}
.controls { .controls {
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
@@ -4,7 +4,6 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal'; import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector'; import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar'; import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
@@ -64,15 +63,6 @@ class AuthorIndexPoster extends Component {
} }
} }
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
// //
// Render // Render
@@ -94,7 +84,6 @@ class AuthorIndexPoster extends Component {
showMonitored, showMonitored,
showQualityProfile, showQualityProfile,
qualityProfile, qualityProfile,
metadataProfile,
showSearchAction, showSearchAction,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
@@ -103,9 +92,6 @@ class AuthorIndexPoster extends Component {
isSearchingAuthor, isSearchingAuthor,
onRefreshAuthorPress, onRefreshAuthorPress,
onSearchPress, onSearchPress,
isEditorActive,
isSelected,
onSelectedChange,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -134,18 +120,6 @@ class AuthorIndexPoster extends Component {
<div> <div>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
<Label className={styles.controls}> <Label className={styles.controls}>
<SpinnerIconButton <SpinnerIconButton
className={styles.action} className={styles.action}
@@ -260,7 +234,6 @@ class AuthorIndexPoster extends Component {
sizeOnDisk={sizeOnDisk} sizeOnDisk={sizeOnDisk}
qualityProfile={qualityProfile} qualityProfile={qualityProfile}
showQualityProfile={showQualityProfile} showQualityProfile={showQualityProfile}
metadataProfile={metadataProfile}
showRelativeDates={showRelativeDates} showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat} shortDateFormat={shortDateFormat}
timeFormat={timeFormat} timeFormat={timeFormat}
@@ -302,7 +275,6 @@ AuthorIndexPoster.propTypes = {
showMonitored: PropTypes.bool.isRequired, showMonitored: PropTypes.bool.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
metadataProfile: PropTypes.object.isRequired,
showSearchAction: PropTypes.bool.isRequired, showSearchAction: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
@@ -310,10 +282,7 @@ AuthorIndexPoster.propTypes = {
isRefreshingAuthor: PropTypes.bool.isRequired, isRefreshingAuthor: PropTypes.bool.isRequired,
isSearchingAuthor: PropTypes.bool.isRequired, isSearchingAuthor: PropTypes.bool.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired, onRefreshAuthorPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
}; };
AuthorIndexPoster.defaultProps = { AuthorIndexPoster.defaultProps = {
@@ -8,10 +8,8 @@ function AuthorIndexPosterInfo(props) {
const { const {
qualityProfile, qualityProfile,
showQualityProfile, showQualityProfile,
metadataProfile, previousAiring,
added, added,
nextBook,
lastBook,
bookCount, bookCount,
path, path,
sizeOnDisk, sizeOnDisk,
@@ -29,10 +27,20 @@ function AuthorIndexPosterInfo(props) {
); );
} }
if (sortKey === 'metadataProfileId') { if (sortKey === 'previousAiring' && previousAiring) {
return ( return (
<div className={styles.info}> <div className={styles.info}>
{metadataProfile.name} {
getRelativeDate(
previousAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
</div> </div>
); );
} }
@@ -55,42 +63,6 @@ function AuthorIndexPosterInfo(props) {
); );
} }
if (sortKey === 'nextBook' && nextBook) {
const date = getRelativeDate(
nextBook.releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
return (
<div className={styles.info}>
{`Next Book ${date}`}
</div>
);
}
if (sortKey === 'lastBook' && lastBook) {
const date = getRelativeDate(
lastBook.releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
return (
<div className={styles.info}>
{`Last Book ${date}`}
</div>
);
}
if (sortKey === 'bookCount') { if (sortKey === 'bookCount') {
let books = '1 book'; let books = '1 book';
@@ -129,10 +101,8 @@ function AuthorIndexPosterInfo(props) {
AuthorIndexPosterInfo.propTypes = { AuthorIndexPosterInfo.propTypes = {
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
metadataProfile: PropTypes.object.isRequired, previousAiring: PropTypes.string,
added: PropTypes.string, added: PropTypes.string,
nextBook: PropTypes.object,
lastBook: PropTypes.object,
bookCount: PropTypes.number.isRequired, bookCount: PropTypes.number.isRequired,
path: PropTypes.string.isRequired, path: PropTypes.string.isRequired,
sizeOnDisk: PropTypes.number, sizeOnDisk: PropTypes.number,
@@ -116,9 +116,7 @@ class AuthorIndexPosters extends Component {
posterOptions, posterOptions,
jumpToCharacter, jumpToCharacter,
isSmallScreen, isSmallScreen,
isEditorActive, scrollTop
scrollTop,
selectedState
} = this.props; } = this.props;
const { const {
@@ -140,8 +138,6 @@ class AuthorIndexPosters extends Component {
prevState.columnCount !== columnCount || prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items)) || hasDifferentItemsOrOrder(prevProps.items, items)) ||
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState ||
prevProps.posterOptions.showTitle !== posterOptions.showTitle) { prevProps.posterOptions.showTitle !== posterOptions.showTitle) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
@@ -202,10 +198,7 @@ class AuthorIndexPosters extends Component {
posterOptions, posterOptions,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat, timeFormat
selectedState,
isEditorActive,
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -253,9 +246,6 @@ class AuthorIndexPosters extends Component {
authorId={author.id} authorId={author.id}
qualityProfileId={author.qualityProfileId} qualityProfileId={author.qualityProfileId}
metadataProfileId={author.metadataProfileId} metadataProfileId={author.metadataProfileId}
isSelected={selectedState[author.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/> />
</div> </div>
); );
@@ -338,10 +328,7 @@ AuthorIndexPosters.propTypes = {
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
}; };
export default AuthorIndexPosters; export default AuthorIndexPosters;
@@ -5,7 +5,6 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import AuthorIndexTableOptionsConnector from './AuthorIndexTableOptionsConnector'; import AuthorIndexTableOptionsConnector from './AuthorIndexTableOptionsConnector';
import hasGrowableColumns from './hasGrowableColumns'; import hasGrowableColumns from './hasGrowableColumns';
@@ -16,10 +15,6 @@ function AuthorIndexHeader(props) {
showBanners, showBanners,
columns, columns,
onTableOptionChange, onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
...otherProps ...otherProps
} = props; } = props;
@@ -38,21 +33,6 @@ function AuthorIndexHeader(props) {
return null; return null;
} }
if (name === 'select') {
if (isEditorActive) {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
return null;
}
if (name === 'actions') { if (name === 'actions') {
return ( return (
<VirtualTableHeaderCell <VirtualTableHeaderCell
@@ -100,10 +80,6 @@ function AuthorIndexHeader(props) {
AuthorIndexHeader.propTypes = { AuthorIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired, onTableOptionChange: PropTypes.func.isRequired,
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired,
showBanners: PropTypes.bool.isRequired showBanners: PropTypes.bool.isRequired
}; };
@@ -13,7 +13,6 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar'; import ProgressBar from 'Components/ProgressBar';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import getProgressBarKind from 'Utilities/Author/getProgressBarKind'; import getProgressBarKind from 'Utilities/Author/getProgressBarKind';
@@ -102,11 +101,8 @@ class AuthorIndexRow extends Component {
columns, columns,
isRefreshingAuthor, isRefreshingAuthor,
isSearchingAuthor, isSearchingAuthor,
isEditorActive,
isSelected,
onRefreshAuthorPress, onRefreshAuthorPress,
onSearchPress, onSearchPress
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -135,19 +131,6 @@ class AuthorIndexRow extends Component {
return null; return null;
} }
if (isEditorActive && name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'status') { if (name === 'status') {
return ( return (
<AuthorStatusCell <AuthorStatusCell
@@ -448,10 +431,7 @@ AuthorIndexRow.propTypes = {
isRefreshingAuthor: PropTypes.bool.isRequired, isRefreshingAuthor: PropTypes.bool.isRequired,
isSearchingAuthor: PropTypes.bool.isRequired, isSearchingAuthor: PropTypes.bool.isRequired,
onRefreshAuthorPress: PropTypes.func.isRequired, onRefreshAuthorPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
}; };
AuthorIndexRow.defaultProps = { AuthorIndexRow.defaultProps = {
@@ -48,9 +48,6 @@ class AuthorIndexTable extends Component {
const { const {
items, items,
columns, columns,
selectedState,
onSelectedChange,
isEditorActive,
showBanners, showBanners,
showTitle showTitle
} = this.props; } = this.props;
@@ -70,9 +67,6 @@ class AuthorIndexTable extends Component {
authorId={author.id} authorId={author.id}
qualityProfileId={author.qualityProfileId} qualityProfileId={author.qualityProfileId}
metadataProfileId={author.metadataProfileId} metadataProfileId={author.metadataProfileId}
isSelected={selectedState[author.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
showBanners={showBanners} showBanners={showBanners}
showTitle={showTitle} showTitle={showTitle}
/> />
@@ -93,12 +87,7 @@ class AuthorIndexTable extends Component {
isSmallScreen, isSmallScreen,
onSortPress, onSortPress,
scroller, scroller,
scrollTop, scrollTop
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
selectedState
} = this.props; } = this.props;
return ( return (
@@ -119,13 +108,8 @@ class AuthorIndexTable extends Component {
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onSortPress={onSortPress} onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
isEditorActive={isEditorActive}
/> />
} }
selectedState={selectedState}
columns={columns} columns={columns}
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
@@ -145,13 +129,7 @@ AuthorIndexTable.propTypes = {
scrollTop: PropTypes.number, scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired, scroller: PropTypes.instanceOf(Element).isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
}; };
export default AuthorIndexTable; export default AuthorIndexTable;
@@ -1,6 +1,5 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Alert from 'Components/Alert';
import Form from 'Components/Form/Form'; import Form from 'Components/Form/Form';
import FormGroup from 'Components/Form/FormGroup'; import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup'; import FormInputGroup from 'Components/Form/FormInputGroup';
@@ -11,7 +10,7 @@ import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
@@ -93,12 +92,6 @@ class MonitoringOptionsModalContent extends Component {
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<Alert kind={kinds.INFO}>
<div>
{translate('MonitorBookExistingOnlyWarning')}
</div>
</Alert>
<Form {...otherProps}> <Form {...otherProps}>
<FormGroup> <FormGroup>
<FormLabel>{translate('Monitoring')}</FormLabel> <FormLabel>{translate('Monitoring')}</FormLabel>
@@ -8,7 +8,6 @@ import * as commandNames from 'Commands/commandNames';
import { toggleBooksMonitored } from 'Store/Actions/bookActions'; import { toggleBooksMonitored } from 'Store/Actions/bookActions';
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions'; import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions'; import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector'; import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector'; import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
@@ -44,12 +43,11 @@ function createMapStateToProps() {
(state, { titleSlug }) => titleSlug, (state, { titleSlug }) => titleSlug,
selectBookFiles, selectBookFiles,
(state) => state.books, (state) => state.books,
(state) => state.editions,
createAllAuthorSelector(), createAllAuthorSelector(),
createCommandsSelector(), createCommandsSelector(),
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(titleSlug, bookFiles, books, editions, authors, commands, uiSettings, dimensions) => { (titleSlug, bookFiles, books, authors, commands, uiSettings, dimensions) => {
const book = books.items.find((b) => b.titleSlug === titleSlug); const book = books.items.find((b) => b.titleSlug === titleSlug);
const author = authors.find((a) => a.id === book.authorId); const author = authors.find((a) => a.id === book.authorId);
const sortedBooks = books.items.filter((b) => b.authorId === book.authorId); const sortedBooks = books.items.filter((b) => b.authorId === book.authorId);
@@ -81,8 +79,8 @@ function createMapStateToProps() {
isRefreshingCommand.body.bookId === book.id isRefreshingCommand.body.bookId === book.id
); );
const isFetching = isBookFilesFetching || editions.isFetching; const isFetching = isBookFilesFetching;
const isPopulated = isBookFilesPopulated && editions.isPopulated; const isPopulated = isBookFilesPopulated;
return { return {
...book, ...book,
@@ -106,8 +104,6 @@ const mapDispatchToProps = {
executeCommand, executeCommand,
fetchBookFiles, fetchBookFiles,
clearBookFiles, clearBookFiles,
fetchEditions,
clearEditions,
clearReleases, clearReleases,
cancelFetchReleases, cancelFetchReleases,
toggleBooksMonitored toggleBooksMonitored
@@ -125,8 +121,7 @@ class BookDetailsConnector extends Component {
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id || if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) { (prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
this.unpopulate(); this.unpopulate();
this.populate(); this.populate();
@@ -145,14 +140,12 @@ class BookDetailsConnector extends Component {
const bookId = this.props.id; const bookId = this.props.id;
this.props.fetchBookFiles({ bookId }); this.props.fetchBookFiles({ bookId });
this.props.fetchEditions({ bookId });
} }
unpopulate = () => { unpopulate = () => {
this.props.cancelFetchReleases(); this.props.cancelFetchReleases();
this.props.clearReleases(); this.props.clearReleases();
this.props.clearBookFiles(); this.props.clearBookFiles();
this.props.clearEditions();
} }
// //
@@ -202,8 +195,6 @@ BookDetailsConnector.propTypes = {
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
fetchBookFiles: PropTypes.func.isRequired, fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired, clearBookFiles: PropTypes.func.isRequired,
fetchEditions: PropTypes.func.isRequired,
clearEditions: PropTypes.func.isRequired,
clearReleases: PropTypes.func.isRequired, clearReleases: PropTypes.func.isRequired,
cancelFetchReleases: PropTypes.func.isRequired, cancelFetchReleases: PropTypes.func.isRequired,
toggleBooksMonitored: PropTypes.func.isRequired, toggleBooksMonitored: PropTypes.func.isRequired,
+14 -17
View File
@@ -149,24 +149,21 @@ class BookDetailsHeader extends Component {
</div> </div>
<div className={styles.detailsLabels}> <div className={styles.detailsLabels}>
{ <Label
releaseDate && className={styles.detailsLabel}
<Label size={sizes.LARGE}
className={styles.detailsLabel} >
size={sizes.LARGE} <Icon
> name={icons.CALENDAR}
<Icon size={17}
name={icons.CALENDAR} />
size={17}
/>
<span className={styles.sizeOnDisk}> <span className={styles.sizeOnDisk}>
{ {
moment(releaseDate).format(shortDateFormat) moment(releaseDate).format(shortDateFormat)
} }
</span> </span>
</Label> </Label>
}
<Label <Label
className={styles.detailsLabel} className={styles.detailsLabel}
@@ -8,25 +8,15 @@ import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector'; import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import BookDetailsHeader from './BookDetailsHeader'; import BookDetailsHeader from './BookDetailsHeader';
const selectOverview = createSelector(
(state) => state.editions,
(editions) => {
const monitored = editions.items.find((e) => e.monitored === true);
return monitored?.overview;
}
);
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookSelector(), createBookSelector(),
selectOverview,
createUISettingsSelector(), createUISettingsSelector(),
createDimensionsSelector(), createDimensionsSelector(),
(book, overview, uiSettings, dimensions) => { (book, uiSettings, dimensions) => {
return { return {
...book, ...book,
overview,
shortDateFormat: uiSettings.shortDateFormat, shortDateFormat: uiSettings.shortDateFormat,
isSmallScreen: dimensions.isSmallScreen isSmallScreen: dimensions.isSmallScreen
}; };
+14 -36
View File
@@ -6,13 +6,11 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel'; import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
class EditBookModalContent extends Component { class EditBookModalContent extends Component {
@@ -38,9 +36,6 @@ class EditBookModalContent extends Component {
authorName, authorName,
statistics, statistics,
item, item,
isFetching,
isPopulated,
error,
isSaving, isSaving,
onInputChange, onInputChange,
onModalClose, onModalClose,
@@ -53,8 +48,7 @@ class EditBookModalContent extends Component {
editions editions
} = item; } = item;
const hasFile = statistics ? statistics.bookFileCount > 0 : false; const hasFile = statistics ? statistics.bookFileCount : 0;
const errorMessage = getErrorMessage(error, 'Unable to load editions');
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
@@ -94,33 +88,20 @@ class EditBookModalContent extends Component {
/> />
</FormGroup> </FormGroup>
{ <FormGroup>
isFetching && <FormLabel>
<LoadingIndicator /> {translate('Edition')}
} </FormLabel>
{ <FormInputGroup
error && type={inputTypes.BOOK_EDITION_SELECT}
<div>{errorMessage}</div> name="editions"
} helpText={translate('EditionsHelpText')}
isDisabled={anyEditionOk.value && hasFile}
{ bookEditions={editions}
isPopulated && !isFetching && !!editions.value.length && onChange={onInputChange}
<FormGroup> />
<FormLabel> </FormGroup>
{translate('Edition')}
</FormLabel>
<FormInputGroup
type={inputTypes.BOOK_EDITION_SELECT}
name="editions"
helpText={translate('EditionsHelpText')}
isDisabled={anyEditionOk.value && hasFile}
bookEditions={editions}
onChange={onInputChange}
/>
</FormGroup>
}
</Form> </Form>
</ModalBody> </ModalBody>
@@ -150,9 +131,6 @@ EditBookModalContent.propTypes = {
authorName: PropTypes.string.isRequired, authorName: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired, statistics: PropTypes.object.isRequired,
item: PropTypes.object.isRequired, item: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
error: PropTypes.object,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired, isSaving: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired, onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired, onSavePress: PropTypes.func.isRequired,
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveBook, setBookValue } from 'Store/Actions/bookActions'; import { saveBook, setBookValue } from 'Store/Actions/bookActions';
import { saveEditions } from 'Store/Actions/editionActions';
import createAuthorSelector from 'Store/Selectors/createAuthorSelector'; import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
import createBookSelector from 'Store/Selectors/createBookSelector'; import createBookSelector from 'Store/Selectors/createBookSelector';
import selectSettings from 'Store/Selectors/selectSettings'; import selectSettings from 'Store/Selectors/selectSettings';
@@ -13,27 +12,20 @@ import EditBookModalContent from './EditBookModalContent';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.books, (state) => state.books,
(state) => state.editions,
createBookSelector(), createBookSelector(),
createAuthorSelector(), createAuthorSelector(),
(bookState, editionState, book, author) => { (bookState, book, author) => {
const { const {
isSaving, isSaving,
saveError, saveError,
pendingChanges pendingChanges
} = bookState; } = bookState;
const {
isFetching,
isPopulated,
error
} = editionState;
const bookSettings = _.pick(book, [ const bookSettings = _.pick(book, [
'monitored', 'monitored',
'anyEditionOk' 'anyEditionOk',
'editions'
]); ]);
bookSettings.editions = editionState.items;
const settings = selectSettings(bookSettings, pendingChanges, saveError); const settings = selectSettings(bookSettings, pendingChanges, saveError);
@@ -42,9 +34,6 @@ function createMapStateToProps() {
authorName: author.authorName, authorName: author.authorName,
bookType: book.bookType, bookType: book.bookType,
statistics: book.statistics, statistics: book.statistics,
isFetching,
isPopulated,
error,
isSaving, isSaving,
saveError, saveError,
item: settings.settings, item: settings.settings,
@@ -56,8 +45,7 @@ function createMapStateToProps() {
const mapDispatchToProps = { const mapDispatchToProps = {
dispatchSetBookValue: setBookValue, dispatchSetBookValue: setBookValue,
dispatchSaveBook: saveBook, dispatchSaveBook: saveBook
dispatchSaveEditions: saveEditions
}; };
class EditBookModalContentConnector extends Component { class EditBookModalContentConnector extends Component {
@@ -82,9 +70,6 @@ class EditBookModalContentConnector extends Component {
this.props.dispatchSaveBook({ this.props.dispatchSaveBook({
id: this.props.bookId id: this.props.bookId
}); });
this.props.dispatchSaveEditions({
id: this.props.bookId
});
} }
// //
@@ -107,7 +92,6 @@ EditBookModalContentConnector.propTypes = {
saveError: PropTypes.object, saveError: PropTypes.object,
dispatchSetBookValue: PropTypes.func.isRequired, dispatchSetBookValue: PropTypes.func.isRequired,
dispatchSaveBook: PropTypes.func.isRequired, dispatchSaveBook: PropTypes.func.isRequired,
dispatchSaveEditions: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
@@ -1,70 +0,0 @@
.inputContainer {
margin-right: 20px;
min-width: 150px;
}
.buttonContainer {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.buttonContainerContent {
flex-grow: 0;
}
.buttons {
display: flex;
justify-content: flex-end;
flex-grow: 1;
}
.organizeSelectedButton,
.tagsButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px;
height: 35px;
}
.deleteSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-left: 50px;
height: 35px;
}
@media only screen and (max-width: $breakpointExtraLarge) {
.deleteSelectedButton {
margin-left: 0;
}
}
@media only screen and (max-width: $breakpointLarge) {
.buttonContainer {
justify-content: flex-start;
margin-top: 10px;
}
}
@media only screen and (max-width: $breakpointSmall) {
.inputContainer {
margin-right: 0;
}
.buttonContainer {
justify-content: flex-start;
}
.buttonContainerContent {
flex-grow: 1;
}
.buttons {
justify-content: space-between;
}
.selectedAuthorLabel {
text-align: left;
}
}
@@ -1,156 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
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 BookEditorFooterLabel from './BookEditorFooterLabel';
import DeleteBookModal from './Delete/DeleteBookModal';
import styles from './BookEditorFooter.css';
const NO_CHANGE = 'noChange';
class BookEditorFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitored: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false,
isDeleteBookModalOpen: false,
isTagsModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
rootFolderPath: NO_CHANGE,
savingTags: false
});
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
if (value === NO_CHANGE) {
return;
}
switch (name) {
case 'monitored':
this.props.onSaveSelected({ [name]: value === 'monitored' });
break;
default:
this.props.onSaveSelected({ [name]: value });
}
}
onDeleteSelectedPress = () => {
this.setState({ isDeleteBookModalOpen: true });
}
onDeleteBookModalClose = () => {
this.setState({ isDeleteBookModalOpen: false });
}
//
// Render
render() {
const {
bookIds,
selectedCount,
isSaving,
isDeleting
} = this.props;
const {
monitored,
isDeleteBookModalOpen
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: 'No Change', disabled: true },
{ key: 'monitored', value: 'Monitored' },
{ key: 'unmonitored', value: 'Unmonitored' }
];
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<BookEditorFooterLabel
label={translate('MonitorBook')}
isSaving={isSaving && monitored !== NO_CHANGE}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<BookEditorFooterLabel
label={translate('SelectedCountBooksSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
isSpinning={isDeleting}
isDisabled={!selectedCount || isDeleting}
onPress={this.onDeleteSelectedPress}
>
Delete
</SpinnerButton>
</div>
</div>
</div>
<DeleteBookModal
isOpen={isDeleteBookModalOpen}
bookIds={bookIds}
onModalClose={this.onDeleteBookModalClose}
/>
</PageContentFooter>
);
}
}
BookEditorFooter.propTypes = {
bookIds: PropTypes.arrayOf(PropTypes.number).isRequired,
selectedCount: PropTypes.number.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSaveSelected: PropTypes.func.isRequired
};
export default BookEditorFooter;
@@ -1,8 +0,0 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}
@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import SpinnerIcon from 'Components/SpinnerIcon';
import { icons } from 'Helpers/Props';
import styles from './BookEditorFooterLabel.css';
function BookEditorFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
BookEditorFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
BookEditorFooterLabel.defaultProps = {
className: styles.label
};
export default BookEditorFooterLabel;
@@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import DeleteBookModalContentConnector from './DeleteBookModalContentConnector';
function DeleteBookModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<DeleteBookModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
DeleteBookModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteBookModal;
@@ -1,9 +0,0 @@
.message {
margin-top: 20px;
margin-bottom: 10px;
}
.deleteFilesMessage {
margin-top: 20px;
color: $dangerColor;
}
@@ -1,172 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
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 { inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './DeleteBookModalContent.css';
class DeleteBookModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false,
addImportListExclusion: true
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
}
onAddImportListExclusionChange = ({ value }) => {
this.setState({ addImportListExclusion: value });
}
onDeleteBookConfirmed = () => {
const {
deleteFiles,
addImportListExclusion
} = this.state;
this.setState({ deleteFiles: false });
this.props.onDeleteSelectedPress(deleteFiles, addImportListExclusion);
}
//
// Render
render() {
const {
book,
files,
onModalClose
} = this.props;
const {
deleteFiles,
addImportListExclusion
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Delete Selected Book
</ModalHeader>
<ModalBody>
<div>
<FormGroup>
<FormLabel>{`Delete File${book.length > 1 ? 's' : ''}`}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="deleteFiles"
value={deleteFiles}
helpText={translate('DeleteFilesHelpText')}
kind={kinds.DANGER}
isDisabled={files.length === 0}
onChange={this.onDeleteFilesChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('AddListExclusion')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="addImportListExclusion"
value={addImportListExclusion}
helpText={translate('AddImportListExclusionHelpText')}
kind={kinds.DANGER}
onChange={this.onAddImportListExclusionChange}
/>
</FormGroup>
{
!addImportListExclusion &&
<div className={styles.deleteFilesMessage}>
<div>
{translate('IfYouDontAddAnImportListExclusionAndTheAuthorHasAMetadataProfileOtherThanNoneThenThisBookMayBeReaddedDuringTheNextAuthorRefresh')}
</div>
</div>
}
</div>
<div className={styles.message}>
{`Are you sure you want to delete ${book.length} selected book${book.length > 1 ? 's' : ''}${deleteFiles ? ' and their files' : ''}?`}
</div>
<ul>
{
book.map((s) => {
return (
<li key={s.title}>
<span>{s.title}</span>
</li>
);
})
}
</ul>
{
deleteFiles &&
<div>
<div className={styles.deleteFilesMessage}>
{translate('TheFollowingFilesWillBeDeleted')}
</div>
<ul>
{
files.map((s) => {
return (
<li key={s.path}>
<span>{s.path}</span>
</li>
);
})
}
</ul>
</div>
}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
Cancel
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteBookConfirmed}
>
Delete
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteBookModalContent.propTypes = {
book: PropTypes.arrayOf(PropTypes.object).isRequired,
files: PropTypes.arrayOf(PropTypes.object).isRequired,
onModalClose: PropTypes.func.isRequired,
onDeleteSelectedPress: PropTypes.func.isRequired
};
export default DeleteBookModalContent;
@@ -1,54 +0,0 @@
import _ from 'lodash';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { bulkDeleteBook } from 'Store/Actions/bookIndexActions';
import DeleteBookModalContent from './DeleteBookModalContent';
function createMapStateToProps() {
return createSelector(
(state, { bookIds }) => bookIds,
(state) => state.books.items,
(state) => state.bookFiles.items,
(bookIds, allBooks, allBookFiles) => {
const selectedBook = _.intersectionWith(allBooks, bookIds, (s, id) => {
return s.id === id;
});
const sortedBook = _.orderBy(selectedBook, 'title');
const selectedFiles = _.intersectionWith(allBookFiles, bookIds, (s, id) => {
return s.bookId === id;
});
const files = _.orderBy(selectedFiles, ['bookId', 'path']);
const book = _.map(sortedBook, (s) => {
return {
title: s.title,
path: s.path
};
});
return {
book,
files
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onDeleteSelectedPress(deleteFiles, addImportListExclusion) {
dispatch(bulkDeleteBook({
bookIds: props.bookIds,
deleteFiles,
addImportListExclusion
}));
props.onModalClose();
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(DeleteBookModalContent);
+8 -216
View File
@@ -2,9 +2,7 @@ import _ from 'lodash';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import NoAuthor from 'Author/NoAuthor'; import NoAuthor from 'Author/NoAuthor';
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import PageContent from 'Components/Page/PageContent'; import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody'; import PageContentBody from 'Components/Page/PageContentBody';
import PageJumpBar from 'Components/Page/PageJumpBar'; import PageJumpBar from 'Components/Page/PageJumpBar';
@@ -13,13 +11,10 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection'; import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator'; import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import { align, icons, kinds, sortDirections } from 'Helpers/Props'; import { align, icons, sortDirections } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage'; import getErrorMessage from 'Utilities/Object/getErrorMessage';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder'; import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import getSelectedIds from 'Utilities/Table/getSelectedIds';
import selectAll from 'Utilities/Table/selectAll';
import toggleSelected from 'Utilities/Table/toggleSelected';
import BookIndexFooterConnector from './BookIndexFooterConnector'; import BookIndexFooterConnector from './BookIndexFooterConnector';
import BookIndexFilterMenu from './Menus/BookIndexFilterMenu'; import BookIndexFilterMenu from './Menus/BookIndexFilterMenu';
import BookIndexSortMenu from './Menus/BookIndexSortMenu'; import BookIndexSortMenu from './Menus/BookIndexSortMenu';
@@ -57,19 +52,12 @@ class BookIndex extends Component {
jumpBarItems: { order: [] }, jumpBarItems: { order: [] },
jumpToCharacter: null, jumpToCharacter: null,
isPosterOptionsModalOpen: false, isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false, isOverviewOptionsModalOpen: false
isConfirmSearchModalOpen: false,
isEditorActive: false,
allSelected: false,
allUnselected: false,
lastToggled: null,
selectedState: {}
}; };
} }
componentDidMount() { componentDidMount() {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
componentDidUpdate(prevProps) { componentDidUpdate(prevProps) {
@@ -84,7 +72,6 @@ class BookIndex extends Component {
hasDifferentItemsOrOrder(prevProps.items, items) hasDifferentItemsOrOrder(prevProps.items, items)
) { ) {
this.setJumpBarItems(); this.setJumpBarItems();
this.setSelectedState();
} }
if (this.state.jumpToCharacter != null) { if (this.state.jumpToCharacter != null) {
@@ -99,48 +86,6 @@ class BookIndex extends Component {
this.setState({ scroller: ref }); this.setState({ scroller: ref });
} }
getSelectedIds = () => {
if (this.state.allUnselected) {
return [];
}
return getSelectedIds(this.state.selectedState);
}
setSelectedState() {
const {
items
} = this.props;
const {
selectedState
} = this.state;
const newSelectedState = {};
items.forEach((book) => {
const isItemSelected = selectedState[book.id];
if (isItemSelected) {
newSelectedState[book.id] = isItemSelected;
} else {
newSelectedState[book.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 });
}
setJumpBarItems() { setJumpBarItems() {
const { const {
items, items,
@@ -205,64 +150,10 @@ class BookIndex extends Component {
this.setState({ isOverviewOptionsModalOpen: false }); this.setState({ isOverviewOptionsModalOpen: false });
} }
onEditorTogglePress = () => {
if (this.state.isEditorActive) {
this.setState({ isEditorActive: false });
} else {
const newState = selectAll(this.state.selectedState, false);
newState.isEditorActive = true;
this.setState(newState);
}
}
onJumpBarItemPress = (jumpToCharacter) => { onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter }); this.setState({ jumpToCharacter });
} }
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
}
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
}
onSaveSelected = (changes) => {
this.props.onSaveSelected({
bookIds: this.getSelectedIds(),
...changes
});
}
onSearchPress = () => {
this.setState({ isConfirmSearchModalOpen: true });
}
onRefreshBookPress = () => {
const selectedIds = this.getSelectedIds();
const refreshIds = this.state.isEditorActive && selectedIds.length > 0 ? selectedIds : [];
this.props.onRefreshBookPress(refreshIds);
}
onSearchConfirmed = () => {
const selectedMovieIds = this.getSelectedIds();
const searchIds = this.state.isMovieEditorActive && selectedMovieIds.length > 0 ? selectedMovieIds : this.props.items.map((m) => m.id);
this.props.onSearchPress(searchIds);
this.setState({ isConfirmSearchModalOpen: false });
}
onConfirmSearchModalClose = () => {
this.setState({ isConfirmSearchModalOpen: false });
}
// //
// Render // Render
@@ -282,15 +173,11 @@ class BookIndex extends Component {
view, view,
isRefreshingBook, isRefreshingBook,
isRssSyncExecuting, isRssSyncExecuting,
isSearching,
isSaving,
saveError,
isDeleting,
deleteError,
onScroll, onScroll,
onSortSelect, onSortSelect,
onFilterSelect, onFilterSelect,
onViewSelect, onViewSelect,
onRefreshAuthorPress,
onRssSyncPress, onRssSyncPress,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -300,35 +187,23 @@ class BookIndex extends Component {
jumpBarItems, jumpBarItems,
jumpToCharacter, jumpToCharacter,
isPosterOptionsModalOpen, isPosterOptionsModalOpen,
isOverviewOptionsModalOpen, isOverviewOptionsModalOpen
isConfirmSearchModalOpen,
isEditorActive,
selectedState,
allSelected,
allUnselected
} = this.state; } = this.state;
const selectedBookIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view); const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller); const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoAuthor = !totalItems; const hasNoAuthor = !totalItems;
const refreshLabel = isEditorActive && selectedBookIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
const searchIndexLabel = selectedFilterKey === 'all' ? translate('SearchAll') : translate('SearchFiltered');
const searchEditorLabel = selectedBookIds.length > 0 ? translate('SearchSelected') : translate('SearchAll');
const searchWarningCount = isEditorActive && selectedBookIds.length > 0 ? selectedBookIds.length : items.length;
return ( return (
<PageContent> <PageContent>
<PageToolbar> <PageToolbar>
<PageToolbarSection> <PageToolbarSection>
<PageToolbarButton <PageToolbarButton
label={refreshLabel} label={translate('UpdateAll')}
iconName={icons.REFRESH} iconName={icons.REFRESH}
spinningName={icons.REFRESH} spinningName={icons.REFRESH}
isSpinning={isRefreshingBook} isSpinning={isRefreshingBook}
onPress={this.onRefreshBookPress} onPress={onRefreshAuthorPress}
/> />
<PageToolbarButton <PageToolbarButton
@@ -339,44 +214,6 @@ class BookIndex extends Component {
onPress={onRssSyncPress} onPress={onRssSyncPress}
/> />
<PageToolbarSeparator />
<PageToolbarButton
label={isEditorActive ? searchEditorLabel : searchIndexLabel}
iconName={icons.SEARCH}
isDisabled={isSearching || !items.length}
onPress={this.onSearchPress}
/>
<PageToolbarSeparator />
{
isEditorActive ?
<PageToolbarButton
label={translate('BookIndex')}
iconName={icons.AUTHOR_CONTINUING}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/> :
<PageToolbarButton
label={translate('BookEditor')}
iconName={icons.EDIT}
isDisabled={hasNoAuthor}
onPress={this.onEditorTogglePress}
/>
}
{
isEditorActive ?
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoAuthor}
onPress={this.onSelectAllPress}
/> :
null
}
</PageToolbarSection> </PageToolbarSection>
<PageToolbarSection <PageToolbarSection
@@ -474,12 +311,6 @@ class BookIndex extends Component {
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter} jumpToCharacter={jumpToCharacter}
isEditorActive={isEditorActive}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps} {...otherProps}
/> />
@@ -505,19 +336,6 @@ class BookIndex extends Component {
} }
</div> </div>
{
isLoaded && isEditorActive &&
<BookEditorFooter
bookIds={selectedBookIds}
selectedCount={selectedBookIds.length}
isSaving={isSaving}
saveError={saveError}
isDeleting={isDeleting}
deleteError={deleteError}
onSaveSelected={this.onSaveSelected}
/>
}
<BookIndexPosterOptionsModal <BookIndexPosterOptionsModal
isOpen={isPosterOptionsModalOpen} isOpen={isPosterOptionsModalOpen}
onModalClose={this.onPosterOptionsModalClose} onModalClose={this.onPosterOptionsModalClose}
@@ -528,25 +346,6 @@ class BookIndex extends Component {
onModalClose={this.onOverviewOptionsModalClose} onModalClose={this.onOverviewOptionsModalClose}
/> />
<ConfirmModal
isOpen={isConfirmSearchModalOpen}
kind={kinds.DANGER}
title={translate('MassBookSearch')}
message={
<div>
<div>
{translate('MassBookSearchWarning', [searchWarningCount])}
</div>
<div>
{translate('ThisCannotBeCancelled')}
</div>
</div>
}
confirmLabel={translate('Search')}
onConfirm={this.onSearchConfirmed}
onCancel={this.onConfirmSearchModalClose}
/>
</PageContent> </PageContent>
); );
} }
@@ -566,21 +365,14 @@ BookIndex.propTypes = {
sortDirection: PropTypes.oneOf(sortDirections.all), sortDirection: PropTypes.oneOf(sortDirections.all),
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
isRefreshingBook: PropTypes.bool.isRequired, isRefreshingBook: PropTypes.bool.isRequired,
isSearching: PropTypes.bool.isRequired,
isRssSyncExecuting: PropTypes.bool.isRequired, isRssSyncExecuting: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
onSortSelect: PropTypes.func.isRequired, onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired, onFilterSelect: PropTypes.func.isRequired,
onViewSelect: PropTypes.func.isRequired, onViewSelect: PropTypes.func.isRequired,
onRefreshBookPress: PropTypes.func.isRequired, onRefreshAuthorPress: PropTypes.func.isRequired,
onRssSyncPress: PropTypes.func.isRequired, onRssSyncPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onScroll: PropTypes.func.isRequired
onScroll: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
}; };
export default BookIndex; export default BookIndex;
+4 -27
View File
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import withScrollPosition from 'Components/withScrollPosition'; import withScrollPosition from 'Components/withScrollPosition';
import { saveBookEditor, setBookFilter, setBookSort, setBookTableOption, setBookView } from 'Store/Actions/bookIndexActions'; import { setBookFilter, setBookSort, setBookTableOption, setBookView } from 'Store/Actions/bookIndexActions';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import scrollPositions from 'Store/scrollPositions'; import scrollPositions from 'Store/scrollPositions';
import createBookClientSideCollectionItemsSelector from 'Store/Selectors/createBookClientSideCollectionItemsSelector'; import createBookClientSideCollectionItemsSelector from 'Store/Selectors/createBookClientSideCollectionItemsSelector';
@@ -19,16 +19,12 @@ function createMapStateToProps() {
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR), createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
createCommandExecutingSelector(commandNames.REFRESH_BOOK), createCommandExecutingSelector(commandNames.REFRESH_BOOK),
createCommandExecutingSelector(commandNames.RSS_SYNC), createCommandExecutingSelector(commandNames.RSS_SYNC),
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
createDimensionsSelector(), createDimensionsSelector(),
( (
book, book,
isRefreshingAuthorCommand, isRefreshingAuthorCommand,
isRefreshingBookCommand, isRefreshingBookCommand,
isRssSyncExecuting, isRssSyncExecuting,
isCutoffBooksSearch,
isMissingBooksSearch,
dimensionsState dimensionsState
) => { ) => {
const isRefreshingBook = isRefreshingBookCommand || isRefreshingAuthorCommand; const isRefreshingBook = isRefreshingBookCommand || isRefreshingAuthorCommand;
@@ -36,7 +32,6 @@ function createMapStateToProps() {
...book, ...book,
isRefreshingBook, isRefreshingBook,
isRssSyncExecuting, isRssSyncExecuting,
isSearching: isCutoffBooksSearch || isMissingBooksSearch,
isSmallScreen: dimensionsState.isSmallScreen isSmallScreen: dimensionsState.isSmallScreen
}; };
} }
@@ -61,14 +56,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(setBookView({ view })); dispatch(setBookView({ view }));
}, },
dispatchSaveBookEditor(payload) { onRefreshAuthorPress() {
dispatch(saveBookEditor(payload));
},
onRefreshBookPress(items) {
dispatch(executeCommand({ dispatch(executeCommand({
name: commandNames.BULK_REFRESH_BOOK, name: commandNames.REFRESH_AUTHOR
bookIds: items
})); }));
}, },
@@ -76,13 +66,6 @@ function createMapDispatchToProps(dispatch, props) {
dispatch(executeCommand({ dispatch(executeCommand({
name: commandNames.RSS_SYNC name: commandNames.RSS_SYNC
})); }));
},
onSearchPress(items) {
dispatch(executeCommand({
name: commandNames.BOOK_SEARCH,
bookIds: items
}));
} }
}; };
} }
@@ -96,10 +79,6 @@ class BookIndexConnector extends Component {
this.props.dispatchSetBookView(view); this.props.dispatchSetBookView(view);
} }
onSaveSelected = (payload) => {
this.props.dispatchSaveBookEditor(payload);
}
onScroll = ({ scrollTop }) => { onScroll = ({ scrollTop }) => {
scrollPositions.bookIndex = scrollTop; scrollPositions.bookIndex = scrollTop;
} }
@@ -113,7 +92,6 @@ class BookIndexConnector extends Component {
{...this.props} {...this.props}
onViewSelect={this.onViewSelect} onViewSelect={this.onViewSelect}
onScroll={this.onScroll} onScroll={this.onScroll}
onSaveSelected={this.onSaveSelected}
/> />
); );
} }
@@ -122,8 +100,7 @@ class BookIndexConnector extends Component {
BookIndexConnector.propTypes = { BookIndexConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired, view: PropTypes.string.isRequired,
dispatchSetBookView: PropTypes.func.isRequired, dispatchSetBookView: PropTypes.func.isRequired
dispatchSaveBookEditor: PropTypes.func.isRequired
}; };
export default withScrollPosition( export default withScrollPosition(
+29 -13
View File
@@ -14,18 +14,16 @@ class BookIndexFooter extends PureComponent {
// Render // Render
render() { render() {
const { book } = this.props; const { author } = this.props;
const count = book.length; const count = author.length;
let books = 0; let books = 0;
let bookFiles = 0; let bookFiles = 0;
let ended = 0;
let continuing = 0;
let monitored = 0; let monitored = 0;
let totalFileSize = 0; let totalFileSize = 0;
const authors = new Set(); author.forEach((s) => {
book.forEach((s) => {
authors.add(s.authorId);
const { statistics = {} } = s; const { statistics = {} } = s;
const { const {
@@ -37,6 +35,12 @@ class BookIndexFooter extends PureComponent {
books += bookCount; books += bookCount;
bookFiles += bookFileCount; bookFiles += bookFileCount;
if (s.status === 'ended') {
ended++;
} else {
continuing++;
}
if (s.monitored) { if (s.monitored) {
monitored++; monitored++;
} }
@@ -100,6 +104,23 @@ class BookIndexFooter extends PureComponent {
</div> </div>
<div className={styles.statistics}> <div className={styles.statistics}>
<DescriptionList>
<DescriptionListItem
title={translate('Authors')}
data={count}
/>
<DescriptionListItem
title={translate('Ended')}
data={ended}
/>
<DescriptionListItem
title={translate('Continuing')}
data={continuing}
/>
</DescriptionList>
<DescriptionList> <DescriptionList>
<DescriptionListItem <DescriptionListItem
title={translate('Monitored')} title={translate('Monitored')}
@@ -113,11 +134,6 @@ class BookIndexFooter extends PureComponent {
</DescriptionList> </DescriptionList>
<DescriptionList> <DescriptionList>
<DescriptionListItem
title={translate('Authors')}
data={authors.size}
/>
<DescriptionListItem <DescriptionListItem
title={translate('Books')} title={translate('Books')}
data={books} data={books}
@@ -145,7 +161,7 @@ class BookIndexFooter extends PureComponent {
} }
BookIndexFooter.propTypes = { BookIndexFooter.propTypes = {
book: PropTypes.arrayOf(PropTypes.object).isRequired author: PropTypes.arrayOf(PropTypes.object).isRequired
}; };
export default BookIndexFooter; export default BookIndexFooter;
@@ -6,18 +6,16 @@ import BookIndexFooter from './BookIndexFooter';
function createUnoptimizedSelector() { function createUnoptimizedSelector() {
return createSelector( return createSelector(
createClientSideCollectionSelector('books', 'bookIndex'), createClientSideCollectionSelector('authors', 'authorIndex'),
(books) => { (authors) => {
return books.items.map((s) => { return authors.items.map((s) => {
const { const {
authorId,
monitored, monitored,
status, status,
statistics statistics
} = s; } = s;
return { return {
authorId,
monitored, monitored,
status, status,
statistics statistics
@@ -27,19 +25,19 @@ function createUnoptimizedSelector() {
); );
} }
function createBookSelector() { function createAuthorSelector() {
return createDeepEqualSelector( return createDeepEqualSelector(
createUnoptimizedSelector(), createUnoptimizedSelector(),
(book) => book (author) => author
); );
} }
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookSelector(), createAuthorSelector(),
(book) => { (author) => {
return { return {
book author
}; };
} }
); );
@@ -5,7 +5,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames'; import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import createBookAuthorSelector from 'Store/Selectors/createBookAuthorSelector';
import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector'; import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector';
import createBookSelector from 'Store/Selectors/createBookSelector'; import createBookSelector from 'Store/Selectors/createBookSelector';
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector'; import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
@@ -33,19 +32,17 @@ function selectShowSearchAction() {
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
createBookSelector(), createBookSelector(),
createBookAuthorSelector(),
createBookQualityProfileSelector(), createBookQualityProfileSelector(),
selectShowSearchAction(), selectShowSearchAction(),
createExecutingCommandsSelector(), createExecutingCommandsSelector(),
( (
book, book,
author,
qualityProfile, qualityProfile,
showSearchAction, showSearchAction,
executingCommands executingCommands
) => { ) => {
// If a book is deleted this selector may fire before the parent // If an book is deleted this selector may fire before the parent
// selectors, which will result in an undefined book, if that happens // selectors, which will result in an undefined book, if that happens
// we want to return early here and again in the render function to avoid // we want to return early here and again in the render function to avoid
// trying to show an book that has no information available. // trying to show an book that has no information available.
@@ -57,7 +54,7 @@ function createMapStateToProps() {
const isRefreshingBook = executingCommands.some((command) => { const isRefreshingBook = executingCommands.some((command) => {
return ( return (
(command.name === commandNames.REFRESH_AUTHOR && (command.name === commandNames.REFRESH_AUTHOR &&
command.body.authorId === book.authorId) || command.body.authorId === book.author.id) ||
(command.name === commandNames.REFRESH_BOOK && (command.name === commandNames.REFRESH_BOOK &&
command.body.bookId === book.id) command.body.bookId === book.id)
); );
@@ -66,7 +63,7 @@ function createMapStateToProps() {
const isSearchingBook = executingCommands.some((command) => { const isSearchingBook = executingCommands.some((command) => {
return ( return (
(command.name === commandNames.AUTHOR_SEARCH && (command.name === commandNames.AUTHOR_SEARCH &&
command.body.authorId === book.authorId) || command.body.authorId === book.author.id) ||
(command.name === commandNames.BOOK_SEARCH && (command.name === commandNames.BOOK_SEARCH &&
command.body.bookIds.includes(book.id)) command.body.bookIds.includes(book.id))
); );
@@ -74,7 +71,6 @@ function createMapStateToProps() {
return { return {
...book, ...book,
author,
qualityProfile, qualityProfile,
showSearchAction, showSearchAction,
isRefreshingBook, isRefreshingBook,
@@ -46,15 +46,6 @@ function BookIndexSortMenu(props) {
Author, Title Author, Title
</SortMenuItem> </SortMenuItem>
<SortMenuItem
name="releaseDate"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
Release Date
</SortMenuItem>
<SortMenuItem <SortMenuItem
name="qualityProfileId" name="qualityProfileId"
sortKey={sortKey} sortKey={sortKey}
@@ -19,13 +19,6 @@ $hoverScale: 1.05;
left: 0; left: 0;
} }
.editorSelect {
position: absolute;
top: 0;
left: 5px;
z-index: 3;
}
.posterContainer { .posterContainer {
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -5,14 +5,12 @@ import AuthorPoster from 'Author/AuthorPoster';
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal'; import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector'; import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar'; import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import dimensions from 'Styles/Variables/dimensions'; import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts'; import fonts from 'Styles/Variables/fonts';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import stripHtml from 'Utilities/String/stripHtml'; import stripHtml from 'Utilities/String/stripHtml';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import BookIndexOverviewInfo from './BookIndexOverviewInfo'; import BookIndexOverviewInfo from './BookIndexOverviewInfo';
@@ -43,26 +41,10 @@ class BookIndexOverview extends Component {
this.state = { this.state = {
isEditAuthorModalOpen: false, isEditAuthorModalOpen: false,
isDeleteAuthorModalOpen: false, isDeleteAuthorModalOpen: false
overview: ''
}; };
} }
componentDidMount() {
const { id } = this.props;
// Note that this component is lazy loaded by the virtualised view.
// We want to avoid storing overviews for *all* books which is
// why it's not put into the redux store
const promise = createAjaxRequest({
url: `/book/${id}/overview`
}).request;
promise.done((data) => {
this.setState({ overview: data.overview });
});
}
// //
// Listeners // Listeners
@@ -85,15 +67,6 @@ class BookIndexOverview extends Component {
this.setState({ isDeleteAuthorModalOpen: false }); this.setState({ isDeleteAuthorModalOpen: false });
} }
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
// //
// Render // Render
@@ -101,6 +74,7 @@ class BookIndexOverview extends Component {
const { const {
id, id,
title, title,
overview,
monitored, monitored,
titleSlug, titleSlug,
nextAiring, nextAiring,
@@ -121,8 +95,6 @@ class BookIndexOverview extends Component {
isSearchingBook, isSearchingBook,
onRefreshBookPress, onRefreshBookPress,
onSearchPress, onSearchPress,
isEditorActive,
isSelected,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -134,7 +106,6 @@ class BookIndexOverview extends Component {
} = statistics; } = statistics;
const { const {
overview,
isEditAuthorModalOpen, isEditAuthorModalOpen,
isDeleteAuthorModalOpen isDeleteAuthorModalOpen
} = this.state; } = this.state;
@@ -154,17 +125,6 @@ class BookIndexOverview extends Component {
<div className={styles.container}> <div className={styles.container}>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
{ {
status === 'ended' && status === 'ended' &&
<div <div
@@ -284,6 +244,7 @@ class BookIndexOverview extends Component {
BookIndexOverview.propTypes = { BookIndexOverview.propTypes = {
id: PropTypes.number.isRequired, id: PropTypes.number.isRequired,
title: PropTypes.string.isRequired, title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
titleSlug: PropTypes.string.isRequired, titleSlug: PropTypes.string.isRequired,
nextAiring: PropTypes.string, nextAiring: PropTypes.string,
@@ -303,10 +264,7 @@ BookIndexOverview.propTypes = {
isRefreshingBook: PropTypes.bool.isRequired, isRefreshingBook: PropTypes.bool.isRequired,
isSearchingBook: PropTypes.bool.isRequired, isSearchingBook: PropTypes.bool.isRequired,
onRefreshBookPress: PropTypes.func.isRequired, onRefreshBookPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
}; };
BookIndexOverview.defaultProps = { BookIndexOverview.defaultProps = {
@@ -73,9 +73,7 @@ class BookIndexOverviews extends Component {
sortKey, sortKey,
overviewOptions, overviewOptions,
jumpToCharacter, jumpToCharacter,
scrollTop, scrollTop
isEditorActive,
selectedState
} = this.props; } = this.props;
const { const {
@@ -93,8 +91,6 @@ class BookIndexOverviews extends Component {
(prevState.width !== width || (prevState.width !== width ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) || hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState ||
prevProps.overviewOptions !== overviewOptions)) { prevProps.overviewOptions !== overviewOptions)) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
@@ -152,10 +148,7 @@ class BookIndexOverviews extends Component {
shortDateFormat, shortDateFormat,
longDateFormat, longDateFormat,
timeFormat, timeFormat,
isSmallScreen, isSmallScreen
selectedState,
isEditorActive,
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -190,9 +183,6 @@ class BookIndexOverviews extends Component {
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
bookId={book.id} bookId={book.id}
authorId={book.authorId} authorId={book.authorId}
isSelected={selectedState[book.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/> />
</div> </div>
); );
@@ -273,10 +263,7 @@ BookIndexOverviews.propTypes = {
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired, longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
}; };
export default BookIndexOverviews; export default BookIndexOverviews;
@@ -78,13 +78,6 @@ $hoverScale: 1.05;
color: $white; color: $white;
} }
.editorSelect {
position: absolute;
top: 10px;
left: 10px;
z-index: 4;
}
.controls { .controls {
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
@@ -5,7 +5,6 @@ import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector'; import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
import EditBookModalConnector from 'Book/Edit/EditBookModalConnector'; import EditBookModalConnector from 'Book/Edit/EditBookModalConnector';
import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar'; import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar';
import CheckInput from 'Components/Form/CheckInput';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
@@ -74,15 +73,6 @@ class BookIndexPoster extends Component {
} }
} }
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
}
// //
// Render // Render
@@ -113,9 +103,6 @@ class BookIndexPoster extends Component {
isSearchingBook, isSearchingBook,
onRefreshBookPress, onRefreshBookPress,
onSearchPress, onSearchPress,
isEditorActive,
isSelected,
onSelectedChange,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -145,18 +132,6 @@ class BookIndexPoster extends Component {
<div> <div>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.posterContainer}> <div className={styles.posterContainer}>
{
isEditorActive &&
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
}
<Label className={styles.controls}> <Label className={styles.controls}>
<SpinnerIconButton <SpinnerIconButton
className={styles.action} className={styles.action}
@@ -334,10 +309,7 @@ BookIndexPoster.propTypes = {
isRefreshingBook: PropTypes.bool.isRequired, isRefreshingBook: PropTypes.bool.isRequired,
isSearchingBook: PropTypes.bool.isRequired, isSearchingBook: PropTypes.bool.isRequired,
onRefreshBookPress: PropTypes.func.isRequired, onRefreshBookPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
}; };
BookIndexPoster.defaultProps = { BookIndexPoster.defaultProps = {
@@ -8,8 +8,8 @@ function BookIndexPosterInfo(props) {
const { const {
qualityProfile, qualityProfile,
showQualityProfile, showQualityProfile,
previousAiring,
added, added,
releaseDate,
author, author,
bookFileCount, bookFileCount,
sizeOnDisk, sizeOnDisk,
@@ -27,6 +27,24 @@ function BookIndexPosterInfo(props) {
); );
} }
if (sortKey === 'previousAiring' && previousAiring) {
return (
<div className={styles.info}>
{
getRelativeDate(
previousAiring,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: true
}
)
}
</div>
);
}
if (sortKey === 'added' && added) { if (sortKey === 'added' && added) {
const addedDate = getRelativeDate( const addedDate = getRelativeDate(
added, added,
@@ -45,24 +63,6 @@ function BookIndexPosterInfo(props) {
); );
} }
if (sortKey === 'releaseDate' && added) {
const date = getRelativeDate(
releaseDate,
shortDateFormat,
showRelativeDates,
{
timeFormat,
timeForToday: false
}
);
return (
<div className={styles.info}>
{`Released ${date}`}
</div>
);
}
if (sortKey === 'bookFileCount') { if (sortKey === 'bookFileCount') {
let books = '1 file'; let books = '1 file';
@@ -101,9 +101,9 @@ function BookIndexPosterInfo(props) {
BookIndexPosterInfo.propTypes = { BookIndexPosterInfo.propTypes = {
qualityProfile: PropTypes.object.isRequired, qualityProfile: PropTypes.object.isRequired,
showQualityProfile: PropTypes.bool.isRequired, showQualityProfile: PropTypes.bool.isRequired,
previousAiring: PropTypes.string,
author: PropTypes.object.isRequired, author: PropTypes.object.isRequired,
added: PropTypes.string, added: PropTypes.string,
releaseDate: PropTypes.string,
bookFileCount: PropTypes.number.isRequired, bookFileCount: PropTypes.number.isRequired,
sizeOnDisk: PropTypes.number, sizeOnDisk: PropTypes.number,
sortKey: PropTypes.string.isRequired, sortKey: PropTypes.string.isRequired,
@@ -121,9 +121,7 @@ class BookIndexPosters extends Component {
posterOptions, posterOptions,
jumpToCharacter, jumpToCharacter,
isSmallScreen, isSmallScreen,
isEditorActive, scrollTop
scrollTop,
selectedState
} = this.props; } = this.props;
const { const {
@@ -144,9 +142,7 @@ class BookIndexPosters extends Component {
prevState.columnWidth !== columnWidth || prevState.columnWidth !== columnWidth ||
prevState.columnCount !== columnCount || prevState.columnCount !== columnCount ||
prevState.rowHeight !== rowHeight || prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items)) || hasDifferentItemsOrOrder(prevProps.items, items))) {
prevProps.isEditorActive !== isEditorActive ||
prevProps.selectedState !== selectedState) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells // recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize(); this._grid.recomputeGridSize();
} }
@@ -206,10 +202,7 @@ class BookIndexPosters extends Component {
posterOptions, posterOptions,
showRelativeDates, showRelativeDates,
shortDateFormat, shortDateFormat,
timeFormat, timeFormat
selectedState,
isEditorActive,
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -258,9 +251,6 @@ class BookIndexPosters extends Component {
style={style} style={style}
bookId={book.id} bookId={book.id}
authorId={book.authorId} authorId={book.authorId}
isSelected={selectedState[book.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/> />
</div> </div>
); );
@@ -343,10 +333,7 @@ BookIndexPosters.propTypes = {
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
}; };
export default BookIndexPosters; export default BookIndexPosters;
@@ -5,7 +5,6 @@ import IconButton from 'Components/Link/IconButton';
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper'; import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader'; import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell'; import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import BookIndexTableOptionsConnector from './BookIndexTableOptionsConnector'; import BookIndexTableOptionsConnector from './BookIndexTableOptionsConnector';
import styles from './BookIndexHeader.css'; import styles from './BookIndexHeader.css';
@@ -14,10 +13,6 @@ function BookIndexHeader(props) {
const { const {
columns, columns,
onTableOptionChange, onTableOptionChange,
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
...otherProps ...otherProps
} = props; } = props;
@@ -36,21 +31,6 @@ function BookIndexHeader(props) {
return null; return null;
} }
if (name === 'select') {
if (isEditorActive) {
return (
<VirtualTableSelectAllHeaderCell
key={name}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
/>
);
}
return null;
}
if (name === 'actions') { if (name === 'actions') {
return ( return (
<VirtualTableHeaderCell <VirtualTableHeaderCell
@@ -95,11 +75,7 @@ function BookIndexHeader(props) {
BookIndexHeader.propTypes = { BookIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired, columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onTableOptionChange: PropTypes.func.isRequired, onTableOptionChange: PropTypes.func.isRequired
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
}; };
export default BookIndexHeader; export default BookIndexHeader;
+2 -22
View File
@@ -11,7 +11,6 @@ import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton'; import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import TagListConnector from 'Components/TagListConnector'; import TagListConnector from 'Components/TagListConnector';
import { icons } from 'Helpers/Props'; import { icons } from 'Helpers/Props';
import formatBytes from 'Utilities/Number/formatBytes'; import formatBytes from 'Utilities/Number/formatBytes';
@@ -101,11 +100,8 @@ class BookIndexRow extends Component {
columns, columns,
isRefreshingBook, isRefreshingBook,
isSearchingBook, isSearchingBook,
isEditorActive,
isSelected,
onRefreshBookPress, onRefreshBookPress,
onSearchPress, onSearchPress
onSelectedChange
} = this.props; } = this.props;
const { const {
@@ -132,19 +128,6 @@ class BookIndexRow extends Component {
return null; return null;
} }
if (isEditorActive && name === 'select') {
return (
<VirtualTableSelectCell
inputClassName={styles.checkInput}
id={id}
key={name}
isSelected={isSelected}
isDisabled={false}
onSelectedChange={onSelectedChange}
/>
);
}
if (name === 'status') { if (name === 'status') {
return ( return (
<BookStatusCell <BookStatusCell
@@ -385,10 +368,7 @@ BookIndexRow.propTypes = {
isRefreshingBook: PropTypes.bool.isRequired, isRefreshingBook: PropTypes.bool.isRequired,
isSearchingBook: PropTypes.bool.isRequired, isSearchingBook: PropTypes.bool.isRequired,
onRefreshBookPress: PropTypes.func.isRequired, onRefreshBookPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired, onSearchPress: PropTypes.func.isRequired
isEditorActive: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired
}; };
BookIndexRow.defaultProps = { BookIndexRow.defaultProps = {
@@ -47,10 +47,7 @@ class BookIndexTable extends Component {
rowRenderer = ({ key, rowIndex, style }) => { rowRenderer = ({ key, rowIndex, style }) => {
const { const {
items, items,
columns, columns
selectedState,
onSelectedChange,
isEditorActive
} = this.props; } = this.props;
const book = items[rowIndex]; const book = items[rowIndex];
@@ -67,9 +64,6 @@ class BookIndexTable extends Component {
columns={columns} columns={columns}
authorId={book.authorId} authorId={book.authorId}
bookId={book.id} bookId={book.id}
isSelected={selectedState[book.id]}
onSelectedChange={onSelectedChange}
isEditorActive={isEditorActive}
/> />
</VirtualTableRow> </VirtualTableRow>
); );
@@ -87,12 +81,7 @@ class BookIndexTable extends Component {
isSmallScreen, isSmallScreen,
onSortPress, onSortPress,
scroller, scroller,
scrollTop, scrollTop
allSelected,
allUnselected,
onSelectAllChange,
isEditorActive,
selectedState
} = this.props; } = this.props;
return ( return (
@@ -112,13 +101,8 @@ class BookIndexTable extends Component {
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
onSortPress={onSortPress} onSortPress={onSortPress}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectAllChange={onSelectAllChange}
isEditorActive={isEditorActive}
/> />
} }
selectedState={selectedState}
columns={columns} columns={columns}
sortKey={sortKey} sortKey={sortKey}
sortDirection={sortDirection} sortDirection={sortDirection}
@@ -136,13 +120,7 @@ BookIndexTable.propTypes = {
scrollTop: PropTypes.number, scrollTop: PropTypes.number,
scroller: PropTypes.instanceOf(Element).isRequired, scroller: PropTypes.instanceOf(Element).isRequired,
isSmallScreen: PropTypes.bool.isRequired, isSmallScreen: PropTypes.bool.isRequired,
onSortPress: PropTypes.func.isRequired, onSortPress: PropTypes.func.isRequired
allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired,
onSelectAllChange: PropTypes.func.isRequired,
isEditorActive: PropTypes.bool.isRequired
}; };
export default BookIndexTable; export default BookIndexTable;
+7 -33
View File
@@ -1,12 +1,10 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import MonitorBooksSelectInput from 'Components/Form/MonitorBooksSelectInput'; import MonitorBooksSelectInput from 'Components/Form/MonitorBooksSelectInput';
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
import SelectInput from 'Components/Form/SelectInput'; import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton'; import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter'; import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './BookshelfFooter.css'; import styles from './BookshelfFooter.css';
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
@@ -21,8 +19,7 @@ class BookshelfFooter extends Component {
this.state = { this.state = {
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitor: NO_CHANGE, monitor: NO_CHANGE
monitorNewItems: NO_CHANGE
}; };
} }
@@ -35,8 +32,7 @@ class BookshelfFooter extends Component {
if (prevProps.isSaving && !isSaving && !saveError) { if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({ this.setState({
monitored: NO_CHANGE, monitored: NO_CHANGE,
monitor: NO_CHANGE, monitor: NO_CHANGE
monitorNewItems: NO_CHANGE
}); });
} }
} }
@@ -51,8 +47,7 @@ class BookshelfFooter extends Component {
onUpdateSelectedPress = () => { onUpdateSelectedPress = () => {
const { const {
monitor, monitor,
monitored, monitored
monitorNewItems
} = this.state; } = this.state;
const changes = {}; const changes = {};
@@ -65,10 +60,6 @@ class BookshelfFooter extends Component {
changes.monitor = monitor; changes.monitor = monitor;
} }
if (monitorNewItems !== NO_CHANGE) {
changes.monitorNewItems = monitorNewItems;
}
this.props.onUpdateSelectedPress(changes); this.props.onUpdateSelectedPress(changes);
} }
@@ -83,8 +74,7 @@ class BookshelfFooter extends Component {
const { const {
monitored, monitored,
monitor, monitor
monitorNewItems
} = this.state; } = this.state;
const monitoredOptions = [ const monitoredOptions = [
@@ -93,9 +83,7 @@ class BookshelfFooter extends Component {
{ key: 'unmonitored', value: 'Unmonitored' } { key: 'unmonitored', value: 'Unmonitored' }
]; ];
const noChanges = monitored === NO_CHANGE && const noChanges = monitored === NO_CHANGE && monitor === NO_CHANGE;
monitor === NO_CHANGE &&
monitorNewItems === NO_CHANGE;
return ( return (
<PageContentFooter> <PageContentFooter>
@@ -115,7 +103,7 @@ class BookshelfFooter extends Component {
<div className={styles.inputContainer}> <div className={styles.inputContainer}>
<div className={styles.label}> <div className={styles.label}>
{translate('MonitorExistingBooks')} Monitor Books
</div> </div>
<MonitorBooksSelectInput <MonitorBooksSelectInput
@@ -127,20 +115,6 @@ class BookshelfFooter extends Component {
/> />
</div> </div>
<div className={styles.inputContainer}>
<div className={styles.label}>
{translate('MonitorNewBooks')}
</div>
<MonitorNewItemsSelectInput
name="monitorNewItems"
value={monitorNewItems}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div> <div>
<div className={styles.label}> <div className={styles.label}>
{selectedCount} Author(s) Selected {selectedCount} Author(s) Selected
@@ -153,7 +127,7 @@ class BookshelfFooter extends Component {
isDisabled={!selectedCount || noChanges} isDisabled={!selectedCount || noChanges}
onPress={this.onUpdateSelectedPress} onPress={this.onUpdateSelectedPress}
> >
{translate('UpdateSelected')} Update Selected
</SpinnerButton> </SpinnerButton>
</div> </div>
</PageContentFooter> </PageContentFooter>
+1 -3
View File
@@ -1,7 +1,7 @@
export const APPLICATION_UPDATE = 'ApplicationUpdate'; export const APPLICATION_UPDATE = 'ApplicationUpdate';
export const BACKUP = 'Backup'; export const BACKUP = 'Backup';
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads'; export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
export const CLEAR_BLOCKLIST = 'ClearBlocklist'; export const CLEAR_BLACKLIST = 'ClearBlacklist';
export const CLEAR_LOGS = 'ClearLog'; export const CLEAR_LOGS = 'ClearLog';
export const CUTOFF_UNMET_BOOK_SEARCH = 'CutoffUnmetBookSearch'; export const CUTOFF_UNMET_BOOK_SEARCH = 'CutoffUnmetBookSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles'; export const DELETE_LOG_FILES = 'DeleteLogFiles';
@@ -12,9 +12,7 @@ export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_BOOK_SEARCH = 'MissingBookSearch'; export const MISSING_BOOK_SEARCH = 'MissingBookSearch';
export const MOVE_AUTHOR = 'MoveAuthor'; export const MOVE_AUTHOR = 'MoveAuthor';
export const REFRESH_AUTHOR = 'RefreshAuthor'; export const REFRESH_AUTHOR = 'RefreshAuthor';
export const BULK_REFRESH_AUTHOR = 'BulkRefreshAuthor';
export const REFRESH_BOOK = 'RefreshBook'; export const REFRESH_BOOK = 'RefreshBook';
export const BULK_REFRESH_BOOK = 'BulkRefreshBook';
export const RENAME_FILES = 'RenameFiles'; export const RENAME_FILES = 'RenameFiles';
export const RENAME_AUTHOR = 'RenameAuthor'; export const RENAME_AUTHOR = 'RenameAuthor';
export const RESCAN_FOLDERS = 'RescanFolders'; export const RESCAN_FOLDERS = 'RescanFolders';
@@ -160,7 +160,6 @@ class DateFilterBuilderRowValue extends Component {
<TextInput <TextInput
name={NAME} name={NAME}
value={filterValue} value={filterValue}
type="date"
placeholder="yyyy-mm-dd" placeholder="yyyy-mm-dd"
onChange={this.onValueChange} onChange={this.onValueChange}
/> />
@@ -8,7 +8,6 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import FilterBuilderRow from './FilterBuilderRow'; import FilterBuilderRow from './FilterBuilderRow';
import styles from './FilterBuilderModalContent.css'; import styles from './FilterBuilderModalContent.css';
@@ -166,9 +165,7 @@ class FilterBuilderModalContent extends Component {
</div> </div>
</div> </div>
<div className={styles.label}> <div className={styles.label}>Filters</div>
{translate('Filters')}
</div>
<div className={styles.rows}> <div className={styles.rows}>
{ {
@@ -12,9 +12,9 @@ import ModalBody from 'Components/Modal/ModalBody';
import Portal from 'Components/Portal'; import Portal from 'Components/Portal';
import Scroller from 'Components/Scroller/Scroller'; import Scroller from 'Components/Scroller/Scroller';
import { icons, scrollDirections, sizes } from 'Helpers/Props'; import { icons, scrollDirections, sizes } from 'Helpers/Props';
import { isMobile as isMobileUtil } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isMobile as isMobileUtil } from 'Utilities/mobile';
import HintedSelectInputOption from './HintedSelectInputOption'; import HintedSelectInputOption from './HintedSelectInputOption';
import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue';
import TextInput from './TextInput'; import TextInput from './TextInput';
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { inputTypes } from 'Helpers/Props'; import { inputTypes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
// import translate from 'Utilities/String/translate'; // import translate from 'Utilities/String/translate';
import AutoCompleteInput from './AutoCompleteInput'; import AutoCompleteInput from './AutoCompleteInput';
import BookEditionSelectInputConnector from './BookEditionSelectInputConnector'; import BookEditionSelectInputConnector from './BookEditionSelectInputConnector';
@@ -17,7 +16,6 @@ import IndexerSelectInputConnector from './IndexerSelectInputConnector';
import KeyValueListInput from './KeyValueListInput'; import KeyValueListInput from './KeyValueListInput';
import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector'; import MetadataProfileSelectInputConnector from './MetadataProfileSelectInputConnector';
import MonitorBooksSelectInput from './MonitorBooksSelectInput'; import MonitorBooksSelectInput from './MonitorBooksSelectInput';
import MonitorNewItemsSelectInput from './MonitorNewItemsSelectInput';
import NumberInput from './NumberInput'; import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector'; import OAuthInputConnector from './OAuthInputConnector';
import PasswordInput from './PasswordInput'; import PasswordInput from './PasswordInput';
@@ -53,9 +51,6 @@ function getComponent(type) {
case inputTypes.MONITOR_BOOKS_SELECT: case inputTypes.MONITOR_BOOKS_SELECT:
return MonitorBooksSelectInput; return MonitorBooksSelectInput;
case inputTypes.MONITOR_NEW_ITEMS_SELECT:
return MonitorNewItemsSelectInput;
case inputTypes.NUMBER: case inputTypes.NUMBER:
return NumberInput; return NumberInput;
@@ -217,7 +212,7 @@ function FormInputGroup(props) {
<Link <Link
to={helpLink} to={helpLink}
> >
{translate('MoreInfo')} More Info
</Link> </Link>
} }
@@ -7,7 +7,6 @@ function MonitorBooksSelectInput(props) {
const { const {
includeNoChange, includeNoChange,
includeMixed, includeMixed,
includeSpecificBook,
...otherProps ...otherProps
} = props; } = props;
@@ -29,13 +28,6 @@ function MonitorBooksSelectInput(props) {
}); });
} }
if (includeSpecificBook) {
values.push({
key: 'specificBook',
value: 'Only This Book'
});
}
return ( return (
<SelectInput <SelectInput
values={values} values={values}
@@ -47,14 +39,12 @@ function MonitorBooksSelectInput(props) {
MonitorBooksSelectInput.propTypes = { MonitorBooksSelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired, includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired, includeMixed: PropTypes.bool.isRequired,
includeSpecificBook: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired onChange: PropTypes.func.isRequired
}; };
MonitorBooksSelectInput.defaultProps = { MonitorBooksSelectInput.defaultProps = {
includeNoChange: false, includeNoChange: false,
includeMixed: false, includeMixed: false
includeSpecificBook: false
}; };
export default MonitorBooksSelectInput; export default MonitorBooksSelectInput;
@@ -1,50 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import monitorNewItemsOptions from 'Utilities/Author/monitorNewItemsOptions';
import SelectInput from './SelectInput';
function MonitorNewItemsSelectInput(props) {
const {
includeNoChange,
includeMixed,
...otherProps
} = props;
const values = [...monitorNewItemsOptions];
if (includeNoChange) {
values.unshift({
key: 'noChange',
value: 'No Change',
disabled: true
});
}
if (includeMixed) {
values.unshift({
key: 'mixed',
value: '(Mixed)',
disabled: true
});
}
return (
<SelectInput
values={values}
{...otherProps}
/>
);
}
MonitorNewItemsSelectInput.propTypes = {
includeNoChange: PropTypes.bool.isRequired,
includeMixed: PropTypes.bool.isRequired,
onChange: PropTypes.func.isRequired
};
MonitorNewItemsSelectInput.defaultProps = {
includeNoChange: false,
includeMixed: false
};
export default MonitorNewItemsSelectInput;
@@ -33,7 +33,7 @@ function ConfirmModal(props) {
return () => unbindShortcut('enter', onConfirm); return () => unbindShortcut('enter', onConfirm);
} }
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]); }, [isOpen, onConfirm]);
return ( return (
<Modal <Modal
+1 -1
View File
@@ -6,9 +6,9 @@ import ReactDOM from 'react-dom';
import FocusLock from 'react-focus-lock'; import FocusLock from 'react-focus-lock';
import ErrorBoundary from 'Components/Error/ErrorBoundary'; import ErrorBoundary from 'Components/Error/ErrorBoundary';
import { sizes } from 'Helpers/Props'; import { sizes } from 'Helpers/Props';
import { isIOS } from 'Utilities/browser';
import * as keyCodes from 'Utilities/Constants/keyCodes'; import * as keyCodes from 'Utilities/Constants/keyCodes';
import getUniqueElememtId from 'Utilities/getUniqueElementId'; import getUniqueElememtId from 'Utilities/getUniqueElementId';
import { isIOS } from 'Utilities/mobile';
import { setScrollLock } from 'Utilities/scrollLock'; import { setScrollLock } from 'Utilities/scrollLock';
import ModalError from './ModalError'; import ModalError from './ModalError';
import styles from './Modal.css'; import styles from './Modal.css';
+1 -1
View File
@@ -14,7 +14,7 @@ function PageContent(props) {
return ( return (
<ErrorBoundary errorComponent={PageContentError}> <ErrorBoundary errorComponent={PageContentError}>
<DocumentTitle title={title ? `${title} - ${window.Readarr.instanceName}` : window.Readarr.instanceName}> <DocumentTitle title={title ? `${title} - Readarr` : 'Readarr'}>
<div className={className}> <div className={className}>
{children} {children}
</div> </div>

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