mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-17 21:25:39 -04:00
Compare commits
1 Commits
sonarr-pul
...
book-monit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bd74098f9 |
@@ -19,10 +19,10 @@ indent_size = 4
|
||||
dotnet_sort_system_directives_first = true
|
||||
|
||||
# Avoid "this." and "Me." if not necessary
|
||||
dotnet_style_qualification_for_field = false:warning
|
||||
dotnet_style_qualification_for_property = false:warning
|
||||
dotnet_style_qualification_for_method = false:warning
|
||||
dotnet_style_qualification_for_event = false:warning
|
||||
dotnet_style_qualification_for_field = false:refactoring
|
||||
dotnet_style_qualification_for_property = false:refactoring
|
||||
dotnet_style_qualification_for_method = false:refactoring
|
||||
dotnet_style_qualification_for_event = false:refactoring
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
@@ -32,6 +32,10 @@ csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = flush_left
|
||||
|
||||
dotnet_style_qualification_for_field = false:suggestion
|
||||
dotnet_style_qualification_for_property = false:suggestion
|
||||
dotnet_style_qualification_for_method = false:suggestion
|
||||
dotnet_style_qualification_for_event = false:suggestion
|
||||
dotnet_naming_style.instance_field_style.capitalization = camel_case
|
||||
dotnet_naming_style.instance_field_style.required_prefix = _
|
||||
|
||||
|
||||
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,4 +1,5 @@
|
||||
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'
|
||||
labels: ['Type: Bug', 'Status: Needs Triage']
|
||||
body:
|
||||
@@ -63,11 +64,11 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Trace Logs?
|
||||
label: Anything else?
|
||||
description: |
|
||||
Trace Logs (https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files)
|
||||
***Generally speaking, all bug reports must have trace logs provided.***
|
||||
Links? References? Anything that will give us more context about the issue you are encountering!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
|
||||
validations:
|
||||
required: true
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
1
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,4 +1,5 @@
|
||||
name: Feature Request
|
||||
title: "[FEAT]: "
|
||||
description: 'Suggest an idea for Readarr'
|
||||
labels: ['Type: Feature Request', 'Status: Needs Triage']
|
||||
body:
|
||||
|
||||
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"files.associations": {
|
||||
"*.yaml": "home-assistant"
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
50
README.md
50
README.md
@@ -1,68 +1,56 @@
|
||||
# Readarr
|
||||
|
||||
[](https://dev.azure.com/Readarr/Readarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/readarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/readarr/installation#docker)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
[](#mega-sponsors)
|
||||
[](https://hub.docker.com/r/hotio/readarr)
|
||||
[](#backers) [](#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.
|
||||
|
||||
## Major Features Include
|
||||
## Major Features Include:
|
||||
|
||||
* Can watch for better quality of the ebooks and audiobooks you have and do an automatic upgrade. *e.g. from PDF to AZW3*
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
* Automatically detects new books
|
||||
* Can scan your existing library and download any missing books
|
||||
* Automatic failed download handling will try another release if one fails
|
||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
* Advanced customization for profiles, such that Readarr will always download the copy you want
|
||||
* Fully configurable book renaming
|
||||
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
|
||||
* Full integration with Calibre (add to library, conversion) (Requires Calibre Content Server)
|
||||
* Full integration with SABnzbd and NZBGet
|
||||
* Full integration with Calibre (add to library, conversion)
|
||||
* And a beautiful UI
|
||||
|
||||
## Support
|
||||
|
||||
[](https://wiki.servarr.com/readarr)
|
||||
[](https://readarr.com/discord)
|
||||
[](https://www.reddit.com/r/readarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
[](https://github.com/Readarr/Readarr/issues)
|
||||
[](https://readarr.com/discord)
|
||||
[](https://www.reddit.com/r/readarr)
|
||||
[](https://github.com/Readarr/Readarr/issues)
|
||||
[](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 (GitHub)](CONTRIBUTING.md)
|
||||
- [Contribution (Wiki Article)](https://wiki.servarr.com/readarr/contributing)
|
||||
|
||||
[](https://github.com/Readarr/Readarr/graphs/contributors)
|
||||
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
|
||||
<a href="https://github.com/Readarr/Readarr/graphs/contributors"><img src="https://opencollective.com/Readarr/contributors.svg?width=890&button=false" /></a>
|
||||
|
||||
## Backers
|
||||
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Readarr#backer)
|
||||
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/readarr#backer)
|
||||
|
||||
[](https://opencollective.com/Readarr#backer)
|
||||
<img src="https://opencollective.com/readarr/backers.svg?width=890"></a>
|
||||
|
||||
## Sponsors
|
||||
|
||||
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [Become a sponsor](https://opencollective.com/readarr#sponsor)
|
||||
|
||||
[](https://opencollective.com/readarr#sponsor)
|
||||
<img src="https://opencollective.com/readarr/sponsors.svg?width=890"></a>
|
||||
|
||||
## Mega Sponsors
|
||||
|
||||
[](https://opencollective.com/readarr#mega-sponsor)
|
||||
<img src="https://opencollective.com/readarr/tiers/mega-sponsor.svg?width=890"></a>
|
||||
|
||||
### License
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2022
|
||||
* Copyright 2010-2021
|
||||
|
||||
@@ -7,19 +7,15 @@ variables:
|
||||
outputFolder: './_output'
|
||||
artifactsFolder: './_artifacts'
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.1.1'
|
||||
majorVersion: '0.1.0'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.302'
|
||||
innoVersion: '6.2.0'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
dotnetVersion: '5.0.302'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
@@ -46,7 +42,7 @@ stages:
|
||||
matrix:
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
imageName: 'windows-2019'
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
@@ -66,13 +62,16 @@ stages:
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
|
||||
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
grep osx-x64 $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "BSD already enabled"
|
||||
else
|
||||
echo "Enabling BSD support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Extra Platform Support
|
||||
displayName: Enable FreeBSD Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
@@ -82,29 +81,31 @@ stages:
|
||||
displayName: Build Readarr Backend
|
||||
env:
|
||||
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
|
||||
- publish: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
path: $(outputFolder)
|
||||
artifact: '$(osName)Backend'
|
||||
artifactType: 'pipeline'
|
||||
parallel: true
|
||||
parallelCount: 100
|
||||
displayName: Publish Backend
|
||||
- publish: '$(testsFolder)/net6.0/win-x64/publish'
|
||||
artifact: win-x64-tests
|
||||
displayName: Publish win-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
|
||||
artifact: linux-x64-tests
|
||||
displayName: Publish linux-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
|
||||
artifact: linux-x86-tests
|
||||
displayName: Publish linux-x86 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
|
||||
artifact: linux-musl-x64-tests
|
||||
displayName: Publish linux-musl-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
|
||||
artifact: freebsd-x64-tests
|
||||
displayName: Publish freebsd-x64 Test Package
|
||||
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
|
||||
artifact: osx-x64-tests
|
||||
displayName: Publish osx-x64 Test Package
|
||||
- publish: '$(testsFolder)/net5.0/win-x64/publish'
|
||||
artifact: WindowsCoreTests
|
||||
displayName: Publish Windows Test Package
|
||||
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
|
||||
artifact: LinuxCoreTests
|
||||
displayName: Publish Linux Test Package
|
||||
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
|
||||
artifact: LinuxMuslCoreTests
|
||||
displayName: Publish Linux Musl Test Package
|
||||
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
|
||||
artifact: FreebsdCoreTests
|
||||
displayName: Publish FreeBSD Test Package
|
||||
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
|
||||
artifact: MacCoreTests
|
||||
displayName: Publish MacOS Test Package
|
||||
|
||||
- stage: Build_Backend_Other
|
||||
displayName: Build Backend (Other OS)
|
||||
@@ -115,11 +116,11 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
imageName: 'ubuntu-18.04'
|
||||
enableAnalysis: 'true'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: ${{ variables.macImage }}
|
||||
imageName: 'macos-10.14'
|
||||
enableAnalysis: 'false'
|
||||
|
||||
pool:
|
||||
@@ -136,29 +137,25 @@ stages:
|
||||
inputs:
|
||||
version: $(dotnetVersion)
|
||||
- bash: |
|
||||
SDK_PATH="${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}"
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
|
||||
if ! grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
|
||||
echo $BUNDLEDVERSIONS
|
||||
grep osx-x64 $BUNDLEDVERSIONS
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "BSD already enabled"
|
||||
else
|
||||
echo "Enabling BSD support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
displayName: Extra Platform Support
|
||||
displayName: Enable FreeBSD Support
|
||||
- task: Cache@2
|
||||
inputs:
|
||||
key: 'nuget | "$(Agent.OS)" | $(Build.SourcesDirectory)/src/Directory.Packages.props'
|
||||
path: $(nugetCacheFolder)
|
||||
displayName: Cache NuGet packages
|
||||
- bash: ./build.sh --backend --enable-extra-platforms
|
||||
- bash: ./build.sh --backend --enable-bsd
|
||||
displayName: Build Readarr Backend
|
||||
env:
|
||||
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
|
||||
displayName: Frontend
|
||||
@@ -169,13 +166,13 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
imageName: 'ubuntu-18.04'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
imageName: ${{ variables.macImage }}
|
||||
imageName: 'macos-10.14'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
imageName: 'windows-2019'
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
@@ -212,7 +209,7 @@ stages:
|
||||
- job: Windows_Installer
|
||||
displayName: Create Installer
|
||||
pool:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
vmImage: 'windows-2019'
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -228,11 +225,16 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
./build.sh --packages --installer
|
||||
cp setup/output/Readarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x64-installer.exe
|
||||
cp setup/output/Readarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create Installers
|
||||
setup/inno/ISCC.exe setup/readarr.iss //DFramework=net5.0 //DRuntime=win-x86
|
||||
cp setup/output/Readarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Readarr.${BUILDNAME}.windows-core-x86-installer.exe
|
||||
displayName: Create .NET Core Windows installer
|
||||
- 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)
|
||||
artifact: 'WindowsInstaller'
|
||||
displayName: Publish Installer
|
||||
@@ -245,7 +247,7 @@ stages:
|
||||
- job: Other_Packages
|
||||
displayName: Create Standard Packages
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: 1
|
||||
@@ -261,120 +263,90 @@ stages:
|
||||
artifactName: WindowsFrontend
|
||||
targetPath: _output
|
||||
displayName: Fetch Frontend
|
||||
- bash: ./build.sh --packages --enable-extra-platforms
|
||||
- bash: ./build.sh --packages --enable-bsd
|
||||
displayName: Create Packages
|
||||
- bash: |
|
||||
find . -name "fpcalc" -exec chmod a+x {} \;
|
||||
find . -name "Readarr" -exec chmod a+x {} \;
|
||||
find . -name "Readarr.Update" -exec chmod a+x {} \;
|
||||
displayName: Set executable bits
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create win-x64 zip
|
||||
displayName: Create Windows Core zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create win-x86 zip
|
||||
displayName: Create Windows x86 Core zip
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).windows-core-x86.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/win-x86/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-x64 app
|
||||
displayName: Create MacOS Core app
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-app-core-x64.zip'
|
||||
archiveType: 'zip'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/macos-app/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-x64 tar
|
||||
displayName: Create MacOS Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).osx-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/macos/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create osx-arm64 app
|
||||
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
|
||||
displayName: Create Linux Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-x64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-musl-x64 tar
|
||||
displayName: Create Linux Musl Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-x86 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
|
||||
displayName: Create ARM32 Linux Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-musl-arm 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
|
||||
displayName: Create Linux Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create linux-musl-arm64 tar
|
||||
displayName: Create ARM64 Linux Musl Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).linux-musl-core-arm64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net5.0
|
||||
- task: ArchiveFiles@2
|
||||
displayName: Create freebsd-x64 tar
|
||||
displayName: Create FreeBSD Core Core tar
|
||||
inputs:
|
||||
archiveFile: '$(Build.ArtifactStagingDirectory)/Readarr.$(buildName).freebsd-core-x64.tar.gz'
|
||||
archiveType: 'tar'
|
||||
tarCompression: 'gz'
|
||||
includeRootFolder: false
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net6.0
|
||||
rootFolderOrFile: $(artifactsFolder)/freebsd-x64/net5.0
|
||||
- publish: $(Build.ArtifactStagingDirectory)
|
||||
artifact: 'Packages'
|
||||
displayName: Publish Packages
|
||||
@@ -420,22 +392,22 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'osx-x64'
|
||||
testName: 'MacCore'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.macImage }}
|
||||
imageName: 'macos-10.14'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'win-x64'
|
||||
testName: 'WindowsCore'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
imageName: 'windows-2019'
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'linux-x64'
|
||||
testName: 'LinuxCore'
|
||||
poolName: 'Azure Pipelines'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
imageName: 'ubuntu-18.04'
|
||||
FreebsdCore:
|
||||
osName: 'Linux'
|
||||
testName: 'freebsd-x64'
|
||||
testName: 'FreebsdCore'
|
||||
poolName: 'FreeBSD'
|
||||
imageName:
|
||||
|
||||
@@ -454,11 +426,15 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)-tests'
|
||||
artifactName: '$(testName)Tests'
|
||||
targetPath: $(testsFolder)
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
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 {} \;
|
||||
displayName: Make Test Dummy Executable
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
@@ -482,15 +458,11 @@ stages:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: linux-musl-x64-tests
|
||||
artifactName: LinuxMuslCoreTests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
linux-x86:
|
||||
testName: 'linux-x86'
|
||||
artifactName: linux-x86-tests
|
||||
containerImage: ghcr.io/servarr/testimages:linux-x86
|
||||
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
vmImage: 'ubuntu-18.04'
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -498,15 +470,9 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .NET'
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
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
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -529,57 +495,6 @@ stages:
|
||||
testResultsFiles: '**/TestResult.xml'
|
||||
testRunTitle: '$(testName) Unit Tests'
|
||||
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
|
||||
displayName: Integration
|
||||
@@ -591,18 +506,18 @@ stages:
|
||||
matrix:
|
||||
MacCore:
|
||||
osName: 'Mac'
|
||||
testName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
testName: 'MacCore'
|
||||
imageName: 'macos-10.14'
|
||||
pattern: 'Readarr.*.osx-core-x64.tar.gz'
|
||||
WindowsCore:
|
||||
osName: 'Windows'
|
||||
testName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
testName: 'WindowsCore'
|
||||
imageName: 'windows-2019'
|
||||
pattern: 'Readarr.*.windows-core-x64.zip'
|
||||
LinuxCore:
|
||||
osName: 'Linux'
|
||||
testName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
testName: 'LinuxCore'
|
||||
imageName: 'ubuntu-18.04'
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
|
||||
pool:
|
||||
@@ -618,7 +533,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(testName)-tests'
|
||||
artifactName: '$(testName)Tests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -648,66 +563,6 @@ stages:
|
||||
failTaskOnFailedTests: true
|
||||
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
|
||||
displayName: Integration Native FreeBSD
|
||||
workspace:
|
||||
@@ -723,7 +578,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'freebsd-x64-tests'
|
||||
artifactName: 'FreebsdCoreTests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -757,17 +612,13 @@ stages:
|
||||
strategy:
|
||||
matrix:
|
||||
alpine:
|
||||
testName: 'linux-musl-x64'
|
||||
artifactName: linux-musl-x64-tests
|
||||
testName: 'Musl Net Core'
|
||||
artifactName: LinuxMuslCoreTests
|
||||
containerImage: ghcr.io/servarr/testimages:alpine
|
||||
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:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
vmImage: 'ubuntu-18.04'
|
||||
|
||||
container: $[ variables['containerImage'] ]
|
||||
|
||||
@@ -775,15 +626,9 @@ stages:
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .NET'
|
||||
displayName: 'Install .net core'
|
||||
inputs:
|
||||
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
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Test Artifact
|
||||
@@ -829,18 +674,15 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
artifactName: 'linux-x64'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
imageName: 'ubuntu-18.04'
|
||||
pattern: 'Readarr.*.linux-core-x64.tar.gz'
|
||||
Mac:
|
||||
osName: 'Mac'
|
||||
artifactName: 'osx-x64'
|
||||
imageName: ${{ variables.macImage }}
|
||||
imageName: 'macos-10.14'
|
||||
pattern: 'Readarr.*.osx-core-x64.tar.gz'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
artifactName: 'win-x64'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
imageName: 'windows-2019'
|
||||
pattern: 'Readarr.*.windows-core-x64.zip'
|
||||
|
||||
pool:
|
||||
@@ -856,7 +698,7 @@ stages:
|
||||
displayName: Download Test Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: '$(artifactName)-tests'
|
||||
artifactName: '$(osName)CoreTests'
|
||||
targetPath: $(testsFolder)
|
||||
- task: DownloadPipelineArtifact@2
|
||||
displayName: Download Build Artifact
|
||||
@@ -907,10 +749,10 @@ stages:
|
||||
matrix:
|
||||
Linux:
|
||||
osName: 'Linux'
|
||||
imageName: ${{ variables.linuxImage }}
|
||||
imageName: 'ubuntu-18.04'
|
||||
Windows:
|
||||
osName: 'Windows'
|
||||
imageName: ${{ variables.windowsImage }}
|
||||
imageName: 'windows-2019'
|
||||
pool:
|
||||
vmImage: $(imageName)
|
||||
steps:
|
||||
@@ -939,7 +781,7 @@ stages:
|
||||
displayName: Frontend
|
||||
condition: eq(variables['System.PullRequest.IsFork'], 'False')
|
||||
pool:
|
||||
vmImage: ${{ variables.windowsImage }}
|
||||
vmImage: windows-2019
|
||||
steps:
|
||||
- checkout: self # Need history for Sonar analysis
|
||||
- task: SonarCloudPrepare@1
|
||||
@@ -962,17 +804,13 @@ stages:
|
||||
variables:
|
||||
disable.coverage.autogenerate: 'true'
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
vmImage: ubuntu-18.04
|
||||
|
||||
steps:
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core 2.1'
|
||||
inputs:
|
||||
version: 2.1.815
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core 3.1'
|
||||
inputs:
|
||||
version: 3.1.413
|
||||
- task: UseDotNet@2
|
||||
displayName: 'Install .net core 5.0'
|
||||
inputs:
|
||||
@@ -1000,8 +838,8 @@ stages:
|
||||
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
|
||||
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
|
||||
- bash: |
|
||||
./build.sh --backend -f net6.0 -r linux-x64
|
||||
TEST_DIR=_tests/net6.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
|
||||
./build.sh --backend -f net5.0 -r linux-x64
|
||||
TEST_DIR=_tests/net5.0/linux-x64/publish/ ./test.sh Linux Unit Coverage
|
||||
displayName: Coverage Unit Tests
|
||||
env:
|
||||
NUGET_PACKAGES: $(nugetCacheFolder)
|
||||
@@ -1034,7 +872,7 @@ stages:
|
||||
- job:
|
||||
displayName: Discord Notification
|
||||
pool:
|
||||
vmImage: ${{ variables.linuxImage }}
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
continueOnError: true
|
||||
|
||||
138
build.sh
138
build.sh
@@ -27,22 +27,15 @@ UpdateVersionNumber()
|
||||
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')
|
||||
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
|
||||
echo "Extra platforms already enabled"
|
||||
else
|
||||
echo "Enabling extra platform support"
|
||||
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
|
||||
fi
|
||||
}
|
||||
#todo enable sdk with
|
||||
#SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
|
||||
# BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
|
||||
|
||||
EnableExtraPlatforms()
|
||||
{
|
||||
if grep -qv freebsd-x64 src/Directory.Build.props; then
|
||||
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props
|
||||
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
|
||||
}
|
||||
|
||||
@@ -136,7 +129,7 @@ PackageLinux()
|
||||
|
||||
echo "Adding Readarr.Mono to UpdatePackage"
|
||||
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/libMonoPosixHelper.* $folder/Readarr.Update
|
||||
fi
|
||||
@@ -147,13 +140,12 @@ PackageLinux()
|
||||
PackageMacOS()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating MacOS Package for $framework $runtime"
|
||||
ProgressStart "Creating MacOS Package for $framework"
|
||||
|
||||
local folder=$artifactsFolder/$runtime/$framework/Readarr
|
||||
local folder=$artifactsFolder/macos/$framework/Readarr
|
||||
|
||||
PackageFiles "$folder" "$framework" "$runtime"
|
||||
PackageFiles "$folder" "$framework" "osx-x64"
|
||||
|
||||
echo "Removing Service helpers"
|
||||
rm -f $folder/ServiceUninstall.*
|
||||
@@ -164,7 +156,7 @@ PackageMacOS()
|
||||
|
||||
echo "Adding Readarr.Mono to UpdatePackage"
|
||||
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/libMonoPosixHelper.* $folder/Readarr.Update
|
||||
fi
|
||||
@@ -175,11 +167,10 @@ PackageMacOS()
|
||||
PackageMacOSApp()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
ProgressStart "Creating macOS App Package for $framework $runtime"
|
||||
ProgressStart "Creating macOS App Package for $framework"
|
||||
|
||||
local folder="$artifactsFolder/$runtime-app/$framework"
|
||||
local folder=$artifactsFolder/macos-app/$framework
|
||||
|
||||
rm -rf $folder
|
||||
mkdir -p $folder
|
||||
@@ -187,7 +178,7 @@ PackageMacOSApp()
|
||||
mkdir -p $folder/Readarr.app/Contents/MacOS
|
||||
|
||||
echo "Copying Binaries"
|
||||
cp -r $artifactsFolder/$runtime/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
|
||||
cp -r $artifactsFolder/macos/$framework/Readarr/* $folder/Readarr.app/Contents/MacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $folder/Readarr.app/Contents/MacOS/Readarr.Update
|
||||
@@ -234,38 +225,12 @@ Package()
|
||||
PackageWindows "$framework" "$runtime"
|
||||
;;
|
||||
osx)
|
||||
PackageMacOS "$framework" "$runtime"
|
||||
PackageMacOSApp "$framework" "$runtime"
|
||||
PackageMacOS "$framework"
|
||||
PackageMacOSApp "$framework"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
BuildInstaller()
|
||||
{
|
||||
local framework="$1"
|
||||
local runtime="$2"
|
||||
|
||||
./_inno/ISCC.exe setup/readarr.iss "//DFramework=$framework" "//DRuntime=$runtime"
|
||||
}
|
||||
|
||||
InstallInno()
|
||||
{
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
ProgressEnd "Installed portable Inno Setup"
|
||||
}
|
||||
|
||||
RemoveInno()
|
||||
{
|
||||
rm -rf _inno
|
||||
}
|
||||
|
||||
PackageTests()
|
||||
{
|
||||
local framework="$1"
|
||||
@@ -297,10 +262,8 @@ if [ $# -eq 0 ]; then
|
||||
BACKEND=YES
|
||||
FRONTEND=YES
|
||||
PACKAGES=YES
|
||||
INSTALLER=NO
|
||||
LINT=YES
|
||||
ENABLE_EXTRA_PLATFORMS=NO
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
|
||||
ENABLE_BSD=NO
|
||||
fi
|
||||
|
||||
while [[ $# -gt 0 ]]
|
||||
@@ -312,12 +275,8 @@ case $key in
|
||||
BACKEND=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-bsd|--enable-extra-platforms)
|
||||
ENABLE_EXTRA_PLATFORMS=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--enable-extra-platforms-in-sdk)
|
||||
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
|
||||
--enable-bsd)
|
||||
ENABLE_BSD=YES
|
||||
shift # past argument
|
||||
;;
|
||||
-r|--runtime)
|
||||
@@ -338,10 +297,6 @@ case $key in
|
||||
PACKAGES=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--installer)
|
||||
INSTALLER=YES
|
||||
shift # past argument
|
||||
;;
|
||||
--lint)
|
||||
LINT=YES
|
||||
shift # past argument
|
||||
@@ -361,30 +316,24 @@ esac
|
||||
done
|
||||
set -- "${POSITIONAL[@]}" # restore positional parameters
|
||||
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatformsInSDK
|
||||
fi
|
||||
|
||||
if [ "$BACKEND" = "YES" ];
|
||||
then
|
||||
UpdateVersionNumber
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
then
|
||||
EnableExtraPlatforms
|
||||
EnableBsdSupport
|
||||
fi
|
||||
Build
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
PackageTests "net6.0" "win-x64"
|
||||
PackageTests "net6.0" "win-x86"
|
||||
PackageTests "net6.0" "linux-x64"
|
||||
PackageTests "net6.0" "linux-musl-x64"
|
||||
PackageTests "net6.0" "osx-x64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
PackageTests "net5.0" "win-x64"
|
||||
PackageTests "net5.0" "win-x86"
|
||||
PackageTests "net5.0" "linux-x64"
|
||||
PackageTests "net5.0" "linux-musl-x64"
|
||||
PackageTests "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
then
|
||||
PackageTests "net6.0" "freebsd-x64"
|
||||
PackageTests "net6.0" "linux-x86"
|
||||
PackageTests "net5.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
PackageTests "$FRAMEWORK" "$RID"
|
||||
@@ -413,30 +362,19 @@ then
|
||||
|
||||
if [[ -z "$RID" || -z "$FRAMEWORK" ]];
|
||||
then
|
||||
Package "net6.0" "win-x64"
|
||||
Package "net6.0" "win-x86"
|
||||
Package "net6.0" "linux-x64"
|
||||
Package "net6.0" "linux-musl-x64"
|
||||
Package "net6.0" "linux-arm64"
|
||||
Package "net6.0" "linux-musl-arm64"
|
||||
Package "net6.0" "linux-arm"
|
||||
Package "net6.0" "linux-musl-arm"
|
||||
Package "net6.0" "osx-x64"
|
||||
Package "net6.0" "osx-arm64"
|
||||
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ];
|
||||
Package "net5.0" "win-x64"
|
||||
Package "net5.0" "win-x86"
|
||||
Package "net5.0" "linux-x64"
|
||||
Package "net5.0" "linux-musl-x64"
|
||||
Package "net5.0" "linux-arm64"
|
||||
Package "net5.0" "linux-musl-arm64"
|
||||
Package "net5.0" "linux-arm"
|
||||
Package "net5.0" "osx-x64"
|
||||
if [ "$ENABLE_BSD" = "YES" ];
|
||||
then
|
||||
Package "net6.0" "freebsd-x64"
|
||||
Package "net6.0" "linux-x86"
|
||||
Package "net5.0" "freebsd-x64"
|
||||
fi
|
||||
else
|
||||
Package "$FRAMEWORK" "$RID"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALLER" = "YES" ];
|
||||
then
|
||||
InstallInno
|
||||
BuildInstaller "net6.0" "win-x64"
|
||||
BuildInstaller "net6.0" "win-x86"
|
||||
RemoveInno
|
||||
fi
|
||||
|
||||
5
debian/changelog
vendored
Normal file
5
debian/changelog
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
nzbdrone {version} {branch}; urgency=low
|
||||
|
||||
* Automatic Release.
|
||||
|
||||
-- NzbDrone <contact@nzbdrone.com> Mon, 26 Aug 2013 00:00:00 -0700
|
||||
1
debian/compat
vendored
Normal file
1
debian/compat
vendored
Normal file
@@ -0,0 +1 @@
|
||||
8
|
||||
12
debian/control
vendored
Normal file
12
debian/control
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Sonarr <contact@nzbdrone.com>
|
||||
Source: nzbdrone
|
||||
Homepage: https://readarr.com
|
||||
Vcs-Git: git@github.com:readarr/Readarr.git
|
||||
Vcs-Browser: https://github.com/readarr/Readarr
|
||||
|
||||
Package: nzbdrone
|
||||
Architecture: all
|
||||
Depends: libmono-cil-dev (>= 3.2), sqlite3 (>= 3.7), mediainfo (>= 0.7.52)
|
||||
Description: Readarr is a music collection manager
|
||||
24
debian/copyright
vendored
Executable file
24
debian/copyright
vendored
Executable file
@@ -0,0 +1,24 @@
|
||||
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: nzbdrone
|
||||
Source: https://github.com/readarr/Readarr
|
||||
|
||||
Files: *
|
||||
Copyright: 2010-2016 Readarr <hello@readarr.com>
|
||||
|
||||
License: GPL-3.0+
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
.
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
||||
1
debian/install
vendored
Executable file
1
debian/install
vendored
Executable file
@@ -0,0 +1 @@
|
||||
nzbdrone_bin/* opt/NzbDrone
|
||||
13
debian/rules
vendored
Executable file
13
debian/rules
vendored
Executable 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 $@
|
||||
@@ -19,9 +19,9 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlocklistRowConnector from './BlocklistRowConnector';
|
||||
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
|
||||
class Blocklist extends Component {
|
||||
class Blacklist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -103,8 +103,8 @@ class Blocklist extends Component {
|
||||
columns,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlocklistExecuting,
|
||||
onClearBlocklistPress,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -121,7 +121,7 @@ class Blocklist extends Component {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Blocklist')}>
|
||||
<PageContent title={translate('Blacklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
@@ -135,8 +135,8 @@ class Blocklist extends Component {
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
isSpinning={isClearingBlocklistExecuting}
|
||||
onPress={onClearBlocklistPress}
|
||||
isSpinning={isClearingBlacklistExecuting}
|
||||
onPress={onClearBlacklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
@@ -162,14 +162,14 @@ class Blocklist extends Component {
|
||||
{
|
||||
!isAnyFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToLoadBlocklist')}
|
||||
{translate('UnableToLoadBlacklist')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !error && !items.length &&
|
||||
<div>
|
||||
{translate('NoHistoryBlocklist')}
|
||||
No history blacklist
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ class Blocklist extends Component {
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BlocklistRowConnector
|
||||
<BlacklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
@@ -224,7 +224,7 @@ class Blocklist extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
Blocklist.propTypes = {
|
||||
Blacklist.propTypes = {
|
||||
isAuthorFetching: PropTypes.bool.isRequired,
|
||||
isAuthorPopulated: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
@@ -234,9 +234,9 @@ Blocklist.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlocklistPress: PropTypes.func.isRequired
|
||||
onClearBlacklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Blocklist;
|
||||
export default Blacklist;
|
||||
@@ -4,34 +4,34 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as blocklistActions from 'Store/Actions/blocklistActions';
|
||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Blocklist from './Blocklist';
|
||||
import Blacklist from './Blacklist';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.blocklist,
|
||||
(state) => state.blacklist,
|
||||
(state) => state.authors,
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLOCKLIST),
|
||||
(blocklist, authors, isClearingBlocklistExecuting) => {
|
||||
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
|
||||
(blacklist, authors, isClearingBlacklistExecuting) => {
|
||||
return {
|
||||
isAuthorFetching: authors.isFetching,
|
||||
isAuthorPopulated: authors.isPopulated,
|
||||
isClearingBlocklistExecuting,
|
||||
...blocklist
|
||||
isClearingBlacklistExecuting,
|
||||
...blacklist
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...blocklistActions,
|
||||
...blacklistActions,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class BlocklistConnector extends Component {
|
||||
class BlacklistConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -39,27 +39,27 @@ class BlocklistConnector extends Component {
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchBlocklist,
|
||||
gotoBlocklistFirstPage
|
||||
fetchBlacklist,
|
||||
gotoBlacklistFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchBlocklist();
|
||||
fetchBlacklist();
|
||||
} else {
|
||||
gotoBlocklistFirstPage();
|
||||
gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.isClearingBlocklistExecuting && !this.props.isClearingBlocklistExecuting) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearBlocklist();
|
||||
this.props.clearBlacklist();
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
@@ -67,49 +67,49 @@ class BlocklistConnector extends Component {
|
||||
// Control
|
||||
|
||||
repopulate = () => {
|
||||
this.props.fetchBlocklist();
|
||||
this.props.fetchBlacklist();
|
||||
}
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onFirstPagePress = () => {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
|
||||
onPreviousPagePress = () => {
|
||||
this.props.gotoBlocklistPreviousPage();
|
||||
this.props.gotoBlacklistPreviousPage();
|
||||
}
|
||||
|
||||
onNextPagePress = () => {
|
||||
this.props.gotoBlocklistNextPage();
|
||||
this.props.gotoBlacklistNextPage();
|
||||
}
|
||||
|
||||
onLastPagePress = () => {
|
||||
this.props.gotoBlocklistLastPage();
|
||||
this.props.gotoBlacklistLastPage();
|
||||
}
|
||||
|
||||
onPageSelect = (page) => {
|
||||
this.props.gotoBlocklistPage({ page });
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
}
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlocklistItems({ ids });
|
||||
this.props.removeBlacklistItems({ ids });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlocklistSort({ sortKey });
|
||||
this.props.setBlacklistSort({ sortKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlocklistTableOption(payload);
|
||||
this.props.setBlacklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlocklistFirstPage();
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
onClearBlocklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLOCKLIST });
|
||||
onClearBlacklistPress = () => {
|
||||
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -117,7 +117,7 @@ class BlocklistConnector extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Blocklist
|
||||
<Blacklist
|
||||
onFirstPagePress={this.onFirstPagePress}
|
||||
onPreviousPagePress={this.onPreviousPagePress}
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
@@ -126,30 +126,30 @@ class BlocklistConnector extends Component {
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlocklistPress={this.onClearBlocklistPress}
|
||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BlocklistConnector.propTypes = {
|
||||
BlacklistConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlocklist: PropTypes.func.isRequired,
|
||||
gotoBlocklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlocklistPage: PropTypes.func.isRequired,
|
||||
removeBlocklistItems: PropTypes.func.isRequired,
|
||||
setBlocklistSort: PropTypes.func.isRequired,
|
||||
setBlocklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlocklist: PropTypes.func.isRequired,
|
||||
fetchBlacklist: PropTypes.func.isRequired,
|
||||
gotoBlacklistFirstPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
removeBlacklistItems: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlacklist: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlocklistConnector)
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
|
||||
);
|
||||
@@ -10,7 +10,7 @@ import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class BlocklistDetailsModal extends Component {
|
||||
class BlacklistDetailsModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
@@ -78,7 +78,7 @@ class BlocklistDetailsModal extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
BlocklistDetailsModal.propTypes = {
|
||||
BlacklistDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
@@ -87,4 +87,4 @@ BlocklistDetailsModal.propTypes = {
|
||||
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 { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BlocklistDetailsModal from './BlocklistDetailsModal';
|
||||
import styles from './BlocklistRow.css';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
import styles from './BlacklistRow.css';
|
||||
|
||||
class BlocklistRow extends Component {
|
||||
class BlacklistRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
@@ -142,7 +142,7 @@ class BlocklistRow extends Component {
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
title={translate('RemoveFromBlocklist')}
|
||||
title={translate('RemoveFromBlacklist')}
|
||||
name={icons.REMOVE}
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRemovePress}
|
||||
@@ -155,7 +155,7 @@ class BlocklistRow extends Component {
|
||||
})
|
||||
}
|
||||
|
||||
<BlocklistDetailsModal
|
||||
<BlacklistDetailsModal
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
sourceTitle={sourceTitle}
|
||||
protocol={protocol}
|
||||
@@ -169,7 +169,7 @@ class BlocklistRow extends Component {
|
||||
|
||||
}
|
||||
|
||||
BlocklistRow.propTypes = {
|
||||
BlacklistRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
author: PropTypes.object.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
@@ -184,4 +184,4 @@ BlocklistRow.propTypes = {
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BlocklistRow;
|
||||
export default BlacklistRow;
|
||||
@@ -1,8 +1,8 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeBlocklistItem } from 'Store/Actions/blocklistActions';
|
||||
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import BlocklistRow from './BlocklistRow';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
@@ -18,9 +18,9 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeBlocklistItem({ id: props.id }));
|
||||
dispatch(removeBlacklistItem({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlocklistRow);
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BlacklistRow);
|
||||
@@ -215,13 +215,13 @@ function HistoryDetails(props) {
|
||||
|
||||
switch (reason) {
|
||||
case 'Manual':
|
||||
reasonMessage = translate('FileWasDeletedByViaUI');
|
||||
reasonMessage = 'File was deleted by via UI';
|
||||
break;
|
||||
case 'MissingFromDisk':
|
||||
reasonMessage = translate('MissingFromDisk');
|
||||
reasonMessage = 'Readarr was unable to find the file on disk so it was removed';
|
||||
break;
|
||||
case 'Upgrade':
|
||||
reasonMessage = translate('FileWasDeletedByUpgrade');
|
||||
reasonMessage = 'File was deleted to import an upgrade';
|
||||
break;
|
||||
default:
|
||||
reasonMessage = '';
|
||||
|
||||
@@ -12,11 +12,33 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
class History extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Don't update when fetching has completed if items have changed,
|
||||
// before books start fetching or when books start fetching.
|
||||
|
||||
if (
|
||||
(
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items)
|
||||
) ||
|
||||
(!this.props.isBooksFetching && nextProps.isBooksFetching)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
|
||||
@@ -26,7 +29,9 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...historyActions
|
||||
...historyActions,
|
||||
fetchBooks,
|
||||
clearBooks
|
||||
};
|
||||
|
||||
class HistoryConnector extends Component {
|
||||
@@ -50,9 +55,21 @@ class HistoryConnector extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const bookIds = selectUniqueIds(this.props.items, 'bookId');
|
||||
if (bookIds.length) {
|
||||
this.props.fetchBooks({ bookIds });
|
||||
} else {
|
||||
this.props.clearBooks();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearHistory();
|
||||
this.props.clearBooks();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -133,7 +150,9 @@ HistoryConnector.propTypes = {
|
||||
setHistorySort: PropTypes.func.isRequired,
|
||||
setHistoryFilter: PropTypes.func.isRequired,
|
||||
setHistoryTableOption: PropTypes.func.isRequired,
|
||||
clearHistory: PropTypes.func.isRequired
|
||||
clearHistory: PropTypes.func.isRequired,
|
||||
fetchBooks: PropTypes.func.isRequired,
|
||||
clearBooks: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
|
||||
@@ -23,6 +23,8 @@ function getIconName(eventType) {
|
||||
return icons.RETAG;
|
||||
case 'bookImportIncomplete':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadImported':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
@@ -59,6 +61,8 @@ function getTooltip(eventType, data) {
|
||||
return 'Book file tags updated';
|
||||
case 'bookImportIncomplete':
|
||||
return 'Files downloaded but not all could be imported';
|
||||
case 'downloadImported':
|
||||
return 'Download completed and successfully imported';
|
||||
case 'downloadIgnored':
|
||||
return 'Book Download Ignored';
|
||||
default:
|
||||
|
||||
@@ -223,14 +223,14 @@ class Queue extends Component {
|
||||
{
|
||||
!isRefreshing && hasError &&
|
||||
<div>
|
||||
{translate('FailedToLoadQueue')}
|
||||
Failed to load Queue
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isAllPopulated && !hasError && !items.length &&
|
||||
<div>
|
||||
{translate('QueueIsEmpty')}
|
||||
Queue is empty
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -284,17 +284,6 @@ class Queue extends Component {
|
||||
return !!(item && item.authorId && item.bookId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
|
||||
@@ -4,9 +4,12 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import Queue from './Queue';
|
||||
|
||||
@@ -34,6 +37,8 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
...queueActions,
|
||||
fetchBooks,
|
||||
clearBooks,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
@@ -62,6 +67,16 @@ class QueueConnector extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const bookIds = selectUniqueIds(this.props.items, 'bookId');
|
||||
|
||||
if (bookIds.length) {
|
||||
this.props.fetchBooks({ bookIds });
|
||||
} else {
|
||||
this.props.clearBooks();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.includeUnknownAuthorItems !==
|
||||
prevProps.includeUnknownAuthorItems
|
||||
@@ -72,6 +87,8 @@ class QueueConnector extends Component {
|
||||
|
||||
componentWillUnmount() {
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
this.props.clearQueue();
|
||||
this.props.clearBooks();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -168,6 +185,8 @@ QueueConnector.propTypes = {
|
||||
clearQueue: PropTypes.func.isRequired,
|
||||
grabQueueItems: PropTypes.func.isRequired,
|
||||
removeQueueItems: PropTypes.func.isRequired,
|
||||
fetchBooks: PropTypes.func.isRequired,
|
||||
clearBooks: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -43,14 +43,14 @@ class QueueRow extends Component {
|
||||
this.setState({ isRemoveQueueItemModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveQueueItemModalConfirmed = (blocklist, skipredownload) => {
|
||||
onRemoveQueueItemModalConfirmed = (blacklist, skipredownload) => {
|
||||
const {
|
||||
onRemoveQueueItemPress,
|
||||
onQueueRowModalOpenOrClose
|
||||
} = this.props;
|
||||
|
||||
onQueueRowModalOpenOrClose(false);
|
||||
onRemoveQueueItemPress(blocklist, skipredownload);
|
||||
onRemoveQueueItemPress(blacklist, skipredownload);
|
||||
|
||||
this.setState({ isRemoveQueueItemModalOpen: false });
|
||||
}
|
||||
@@ -357,7 +357,6 @@ class QueueRow extends Component {
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!author}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
|
||||
@@ -22,7 +22,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
blacklist: false,
|
||||
skipredownload: false
|
||||
};
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class RemoveQueueItemModal extends Component {
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
blacklist: false,
|
||||
skipredownload: false
|
||||
});
|
||||
}
|
||||
@@ -45,8 +45,8 @@ class RemoveQueueItemModal extends Component {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onSkipReDownloadChange = ({ value }) => {
|
||||
@@ -72,11 +72,10 @@ class RemoveQueueItemModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipredownload } = this.state;
|
||||
const { remove, blacklist, skipredownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -96,41 +95,37 @@ class RemoveQueueItemModal extends Component {
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('BlocklistRelease')}
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
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>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
blacklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
@@ -169,7 +164,6 @@ RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
blacklist: false,
|
||||
skipredownload: false
|
||||
};
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
blacklist: false,
|
||||
skipredownload: false
|
||||
});
|
||||
}
|
||||
@@ -46,8 +46,8 @@ class RemoveQueueItemsModal extends Component {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onSkipReDownloadChange = ({ value }) => {
|
||||
@@ -73,11 +73,10 @@ class RemoveQueueItemsModal extends Component {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipredownload } = this.state;
|
||||
const { remove, blacklist, skipredownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -97,41 +96,37 @@ class RemoveQueueItemsModal extends Component {
|
||||
Are you sure you want to remove {selectedCount} item{selectedCount > 1 ? 's' : ''} from the queue?
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Add Release{selectedCount > 1 ? 's' : ''} To Blocklist
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
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>
|
||||
|
||||
{
|
||||
blocklist &&
|
||||
blacklist &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('SkipRedownload')}
|
||||
@@ -170,7 +165,6 @@ RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -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 Alert from 'Components/Alert';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function AuthorMonitoringOptionsPopoverContent() {
|
||||
return (
|
||||
<>
|
||||
<Alert>
|
||||
{translate('MonitoringOptionsHelpText')}
|
||||
</Alert>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data={translate('DataAllBooks')}
|
||||
/>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('AllBooks')}
|
||||
data="Monitor all books"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('FutureBooks')}
|
||||
data={translate('DataFutureBooks')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('FutureBooks')}
|
||||
data="Monitor books that have not released yet"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('MissingBooks')}
|
||||
data={translate('DataMissingBooks')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('MissingBooks')}
|
||||
data="Monitor books that do not have files or have not released yet"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('ExistingBooks')}
|
||||
data={translate('DataExistingBooks')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('ExistingBooks')}
|
||||
data="Monitor books that have files or have not released yet"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('FirstBook')}
|
||||
data={translate('DataFirstBook')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('FirstBook')}
|
||||
data="Monitor the first book. All other books will be ignored"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('LatestBook')}
|
||||
data={translate('DataLatestBook')}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
title={translate('LatestBook')}
|
||||
data="Monitor the latest book and future books"
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data={translate('DataNone')}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</>
|
||||
<DescriptionListItem
|
||||
title={translate('None')}
|
||||
data="No books will be monitored"
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
|
||||
|
||||
function App({ store, history }) {
|
||||
return (
|
||||
<DocumentTitle title={window.Readarr.instanceName}>
|
||||
<DocumentTitle title="Readarr">
|
||||
<Provider store={store}>
|
||||
<ConnectedRouter history={history}>
|
||||
<PageConnector>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import BlocklistConnector from 'Activity/Blocklist/BlocklistConnector';
|
||||
import BlacklistConnector from 'Activity/Blacklist/BlacklistConnector';
|
||||
import HistoryConnector from 'Activity/History/HistoryConnector';
|
||||
import QueueConnector from 'Activity/Queue/QueueConnector';
|
||||
import AuthorDetailsPageConnector from 'Author/Details/AuthorDetailsPageConnector';
|
||||
import AuthorEditorConnector from 'Author/Editor/AuthorEditorConnector';
|
||||
import AuthorIndexConnector from 'Author/Index/AuthorIndexConnector';
|
||||
import BookDetailsPageConnector from 'Book/Details/BookDetailsPageConnector';
|
||||
import BookIndexConnector from 'Book/Index/BookIndexConnector';
|
||||
import BookshelfConnector from 'Bookshelf/BookshelfConnector';
|
||||
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
|
||||
import NotFound from 'Components/NotFound';
|
||||
@@ -71,26 +71,19 @@ function AppRoutes(props) {
|
||||
/>
|
||||
}
|
||||
|
||||
<Route
|
||||
path="/authors"
|
||||
component={AuthorIndexConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/add/search"
|
||||
component={AddNewItemConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact={true}
|
||||
path="/shelf"
|
||||
component={BookshelfConnector}
|
||||
path="/authoreditor"
|
||||
component={AuthorEditorConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
exact={true}
|
||||
path="/books"
|
||||
component={BookIndexConnector}
|
||||
path="/bookshelf"
|
||||
component={BookshelfConnector}
|
||||
/>
|
||||
|
||||
<Route
|
||||
@@ -132,8 +125,8 @@ function AppRoutes(props) {
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/activity/blocklist"
|
||||
component={BlocklistConnector}
|
||||
path="/activity/blacklist"
|
||||
component={BlacklistConnector}
|
||||
/>
|
||||
|
||||
{/*
|
||||
|
||||
@@ -39,9 +39,7 @@ function AppUpdatedModalContent(props) {
|
||||
<div>
|
||||
{
|
||||
!update.changes &&
|
||||
<div className={styles.maintenance}>
|
||||
{translate('MaintenanceRelease')}
|
||||
</div>
|
||||
<div className={styles.maintenance}>Maintenance release</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ function AuthorPoster(props) {
|
||||
return (
|
||||
<AuthorImage
|
||||
{...props}
|
||||
coverType="poster"
|
||||
placeholder={posterPlaceholder}
|
||||
/>
|
||||
);
|
||||
@@ -18,8 +19,7 @@ AuthorPoster.propTypes = {
|
||||
};
|
||||
|
||||
AuthorPoster.defaultProps = {
|
||||
size: 250,
|
||||
coverType: 'poster'
|
||||
size: 250
|
||||
};
|
||||
|
||||
export default AuthorPoster;
|
||||
|
||||
@@ -125,7 +125,7 @@ class DeleteAuthorModalContent extends Component {
|
||||
deleteFiles &&
|
||||
<div className={styles.deleteFilesMessage}>
|
||||
<div>
|
||||
{translate('TheAuthorFolderAndAllOfItsContentWillBeDeleted', [path])}
|
||||
{translate('TheAuthorFolderStrongpathstrongAndAllOfItsContentWillBeDeleted')}
|
||||
</div>
|
||||
|
||||
{
|
||||
|
||||
@@ -4,8 +4,6 @@ import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
|
||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
@@ -23,7 +21,6 @@ import InteractiveSearchTable from 'InteractiveSearch/InteractiveSearchTable';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
@@ -31,6 +28,7 @@ import AuthorDetailsHeaderConnector from './AuthorDetailsHeaderConnector';
|
||||
import AuthorDetailsSeasonConnector from './AuthorDetailsSeasonConnector';
|
||||
import AuthorDetailsSeriesConnector from './AuthorDetailsSeriesConnector';
|
||||
import styles from './AuthorDetails.css';
|
||||
import MonitoringOptionsModal from 'Author/MonitoringOptions/MonitoringOptionsModal';
|
||||
|
||||
function getExpandedState(newState) {
|
||||
return {
|
||||
@@ -53,58 +51,15 @@ class AuthorDetails extends Component {
|
||||
isRetagModalOpen: false,
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
isMonitorOptionsModalOpen: false,
|
||||
isEditorActive: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {},
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
selectedTabIndex: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setSelectedState = (items) => {
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((item) => {
|
||||
const isItemSelected = selectedState[item.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[item.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[item.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -159,10 +114,6 @@ class AuthorDetails extends Component {
|
||||
this.setState({ isMonitorOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onBookEditorTogglePress = () => {
|
||||
this.setState({ isEditorActive: !this.state.isEditorActive });
|
||||
}
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
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) => {
|
||||
this.setState({ selectedTabIndex: index });
|
||||
}
|
||||
@@ -234,12 +164,7 @@ class AuthorDetails extends Component {
|
||||
previousAuthor,
|
||||
nextAuthor,
|
||||
onRefreshPress,
|
||||
onSearchPress,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
statistics
|
||||
onSearchPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -249,9 +174,6 @@ class AuthorDetails extends Component {
|
||||
isDeleteAuthorModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
isMonitorOptionsModalOpen,
|
||||
isEditorActive,
|
||||
allSelected,
|
||||
selectedState,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
expandedState,
|
||||
@@ -266,14 +188,12 @@ class AuthorDetails extends Component {
|
||||
expandIcon = icons.EXPAND;
|
||||
}
|
||||
|
||||
const selectedBookIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={authorName}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('RefreshAndScan')}
|
||||
label={translate('RefreshScan')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
title={translate('RefreshInformationAndScanDisk')}
|
||||
@@ -331,33 +251,6 @@ class AuthorDetails extends Component {
|
||||
iconName={icons.DELETE}
|
||||
onPress={this.onDeleteAuthorPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={translate('BookList')}
|
||||
iconName={icons.AUTHOR_CONTINUING}
|
||||
onPress={this.onBookEditorTogglePress}
|
||||
/> :
|
||||
<PageToolbarButton
|
||||
label={translate('BookEditor')}
|
||||
iconName={icons.EDIT}
|
||||
onPress={this.onBookEditorTogglePress}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
isEditorActive ?
|
||||
<PageToolbarButton
|
||||
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
|
||||
iconName={icons.CHECK_SQUARE}
|
||||
onPress={this.onSelectAllPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
@@ -438,35 +331,35 @@ class AuthorDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('BooksTotal', [statistics.totalBookCount])}
|
||||
Books
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('SeriesTotal', [series.length])}
|
||||
Series
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('History')}
|
||||
History
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('Search')}
|
||||
Search
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('FilesTotal', [statistics.bookFileCount])}
|
||||
Files
|
||||
</Tab>
|
||||
|
||||
{
|
||||
@@ -483,11 +376,7 @@ class AuthorDetails extends Component {
|
||||
<AuthorDetailsSeasonConnector
|
||||
authorId={id}
|
||||
isExpanded={true}
|
||||
selectedState={selectedState}
|
||||
onExpandPress={this.onExpandPress}
|
||||
setSelectedState={this.setSelectedState}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
</TabPanel>
|
||||
|
||||
@@ -532,6 +421,7 @@ class AuthorDetails extends Component {
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div className={styles.metadataMessage}>
|
||||
@@ -583,19 +473,6 @@ class AuthorDetails extends Component {
|
||||
onModalClose={this.onMonitorOptionsClose}
|
||||
/>
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isEditorActive &&
|
||||
<BookEditorFooter
|
||||
bookIds={selectedBookIds}
|
||||
selectedCount={selectedBookIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
}
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -615,6 +492,7 @@ AuthorDetails.propTypes = {
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
@@ -631,17 +509,13 @@ AuthorDetails.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorDetails.defaultProps = {
|
||||
statistics: {},
|
||||
tags: []
|
||||
tags: [],
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
export default AuthorDetails;
|
||||
|
||||
@@ -6,8 +6,8 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { toggleAuthorMonitored } from 'Store/Actions/authorActions';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
|
||||
import { saveBookEditor } from 'Store/Actions/bookIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
@@ -22,8 +22,7 @@ import AuthorDetails from './AuthorDetails';
|
||||
|
||||
const selectBooks = createSelector(
|
||||
(state) => state.books,
|
||||
(state) => state.bookIndex,
|
||||
(books, index) => {
|
||||
(books) => {
|
||||
const {
|
||||
items,
|
||||
isFetching,
|
||||
@@ -31,13 +30,6 @@ const selectBooks = createSelector(
|
||||
error
|
||||
} = books;
|
||||
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = index;
|
||||
|
||||
const hasBooks = !!items.length;
|
||||
const hasMonitoredBooks = items.some((e) => e.monitored);
|
||||
|
||||
@@ -46,11 +38,7 @@ const selectBooks = createSelector(
|
||||
isBooksPopulated: isPopulated,
|
||||
booksError: error,
|
||||
hasBooks,
|
||||
hasMonitoredBooks,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
hasMonitoredBooks
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -109,7 +97,7 @@ function createMapStateToProps() {
|
||||
createCommandsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(titleSlug, books, series, bookFiles, allAuthors, commands, dimensions) => {
|
||||
const sortedAuthor = _.orderBy(allAuthors, 'sortNameLastFirst');
|
||||
const sortedAuthor = _.orderBy(allAuthors, 'sortName');
|
||||
const authorIndex = _.findIndex(sortedAuthor, { titleSlug });
|
||||
const author = sortedAuthor[authorIndex];
|
||||
|
||||
@@ -122,11 +110,7 @@ function createMapStateToProps() {
|
||||
isBooksPopulated,
|
||||
booksError,
|
||||
hasBooks,
|
||||
hasMonitoredBooks,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError
|
||||
hasMonitoredBooks
|
||||
} = books;
|
||||
|
||||
const {
|
||||
@@ -186,10 +170,6 @@ function createMapStateToProps() {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
booksError,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
seriesError,
|
||||
bookFilesError,
|
||||
hasBooks,
|
||||
@@ -206,9 +186,10 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchBooks,
|
||||
clearBooks,
|
||||
fetchSeries,
|
||||
clearSeries,
|
||||
saveBookEditor,
|
||||
fetchBookFiles,
|
||||
clearBookFiles,
|
||||
toggleAuthorMonitored,
|
||||
@@ -267,6 +248,7 @@ class AuthorDetailsConnector extends Component {
|
||||
populate = () => {
|
||||
const authorId = this.props.id;
|
||||
|
||||
this.props.fetchBooks({ authorId });
|
||||
this.props.fetchSeries({ authorId });
|
||||
this.props.fetchBookFiles({ authorId });
|
||||
this.props.fetchQueueDetails({ authorId });
|
||||
@@ -274,6 +256,7 @@ class AuthorDetailsConnector extends Component {
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.cancelFetchReleases();
|
||||
this.props.clearBooks();
|
||||
this.props.clearSeries();
|
||||
this.props.clearBookFiles();
|
||||
this.props.clearQueueDetails();
|
||||
@@ -304,10 +287,6 @@ class AuthorDetailsConnector extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.saveBookEditor(payload);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -318,7 +297,6 @@ class AuthorDetailsConnector extends Component {
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
onSearchPress={this.onSearchPress}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -332,9 +310,10 @@ AuthorDetailsConnector.propTypes = {
|
||||
isRefreshing: PropTypes.bool.isRequired,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||
fetchBooks: PropTypes.func.isRequired,
|
||||
clearBooks: PropTypes.func.isRequired,
|
||||
fetchSeries: PropTypes.func.isRequired,
|
||||
clearSeries: PropTypes.func.isRequired,
|
||||
saveBookEditor: PropTypes.func.isRequired,
|
||||
fetchBookFiles: PropTypes.func.isRequired,
|
||||
clearBookFiles: PropTypes.func.isRequired,
|
||||
toggleAuthorMonitored: PropTypes.func.isRequired,
|
||||
|
||||
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import getToggledRange from 'Utilities/Table/getToggledRange';
|
||||
import BookRowConnector from './BookRowConnector';
|
||||
import styles from './AuthorDetailsSeason.css';
|
||||
@@ -22,26 +21,6 @@ class AuthorDetailsSeason extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.setSelectedState(this.props.items);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
setSelectedState
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
setSelectedState(items);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -63,42 +42,26 @@ class AuthorDetailsSeason extends Component {
|
||||
this.props.onMonitorBookPress(_.uniq(bookIds), monitored);
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
const {
|
||||
onSelectedChange,
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
return onSelectedChange(items, id, value, shiftKey);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
isEditorActive,
|
||||
columns,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
onSortPress,
|
||||
onTableOptionChange,
|
||||
selectedState
|
||||
onTableOptionChange
|
||||
} = this.props;
|
||||
|
||||
let titleColumns = columns;
|
||||
if (!isEditorActive) {
|
||||
titleColumns = columns.filter((x) => x.name !== 'select');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.bookType}
|
||||
>
|
||||
<div className={styles.books}>
|
||||
<Table
|
||||
columns={titleColumns}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
@@ -113,9 +76,6 @@ class AuthorDetailsSeason extends Component {
|
||||
columns={columns}
|
||||
{...item}
|
||||
onMonitorBookPress={this.onMonitorBookPress}
|
||||
isEditorActive={isEditorActive}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -132,13 +92,9 @@ AuthorDetailsSeason.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onExpandPress: PropTypes.func.isRequired,
|
||||
setSelectedState: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onMonitorBookPress: PropTypes.func.isRequired,
|
||||
uiSettings: PropTypes.object.isRequired
|
||||
|
||||
@@ -4,22 +4,24 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAuthorDetailsId, setAuthorDetailsSort } from 'Store/Actions/authorDetailsActions';
|
||||
import { setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
|
||||
import { setBooksSort, setBooksTableOption, toggleBooksMonitored } from 'Store/Actions/bookActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import AuthorDetailsSeason from './AuthorDetailsSeason';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('books', 'authorDetails'),
|
||||
(state, { label }) => label,
|
||||
createClientSideCollectionSelector('books'),
|
||||
createAuthorSelector(),
|
||||
createCommandsSelector(),
|
||||
createDimensionsSelector(),
|
||||
createUISettingsSelector(),
|
||||
(books, author, dimensions, uiSettings) => {
|
||||
(label, books, author, commands, dimensions, uiSettings) => {
|
||||
|
||||
const booksInGroup = books.items;
|
||||
|
||||
@@ -45,22 +47,14 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setAuthorDetailsId,
|
||||
setAuthorDetailsSort,
|
||||
toggleBooksMonitored,
|
||||
setBooksTableOption,
|
||||
dispatchSetBookSort: setBooksSort,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class AuthorDetailsSeasonConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.setAuthorDetailsId({ authorId: this.props.authorId });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -69,7 +63,7 @@ class AuthorDetailsSeasonConnector extends Component {
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setAuthorDetailsSort({ sortKey });
|
||||
this.props.dispatchSetBookSort({ sortKey });
|
||||
}
|
||||
|
||||
onMonitorBookPress = (bookIds, monitored) => {
|
||||
@@ -98,8 +92,7 @@ AuthorDetailsSeasonConnector.propTypes = {
|
||||
authorId: PropTypes.number.isRequired,
|
||||
toggleBooksMonitored: PropTypes.func.isRequired,
|
||||
setBooksTableOption: PropTypes.func.isRequired,
|
||||
setAuthorDetailsId: PropTypes.func.isRequired,
|
||||
setAuthorDetailsSort: PropTypes.func.isRequired,
|
||||
dispatchSetBookSort: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -2,15 +2,28 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import BookSearchCellConnector from 'Book/BookSearchCellConnector';
|
||||
import BookTitleLink from 'Book/BookTitleLink';
|
||||
import Label from 'Components/Label';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import StarRating from 'Components/StarRating';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import BookStatus from './BookStatus';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './BookRow.css';
|
||||
|
||||
function getBookCountKind(monitored, bookFileCount, bookCount) {
|
||||
if (bookFileCount === bookCount && bookCount > 0) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return kinds.WARNING;
|
||||
}
|
||||
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
class BookRow extends Component {
|
||||
|
||||
//
|
||||
@@ -56,6 +69,7 @@ class BookRow extends Component {
|
||||
id,
|
||||
authorId,
|
||||
monitored,
|
||||
statistics,
|
||||
releaseDate,
|
||||
title,
|
||||
seriesTitle,
|
||||
@@ -65,15 +79,14 @@ class BookRow extends Component {
|
||||
isSaving,
|
||||
authorMonitored,
|
||||
titleSlug,
|
||||
bookFiles,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
const bookFile = bookFiles[0];
|
||||
const isAvailable = Date.parse(releaseDate) < new Date();
|
||||
const {
|
||||
bookCount,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
} = statistics;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
@@ -88,18 +101,6 @@ class BookRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEditorActive && name === 'select') {
|
||||
return (
|
||||
<TableSelectCell
|
||||
key={name}
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'monitored') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -195,11 +196,15 @@ class BookRow extends Component {
|
||||
key={name}
|
||||
className={styles.status}
|
||||
>
|
||||
<BookStatus
|
||||
isAvailable={isAvailable}
|
||||
monitored={monitored}
|
||||
bookFile={bookFile}
|
||||
/>
|
||||
<Label
|
||||
title={translate('TotalBookCountBooksTotalBookFileCountBooksWithFilesInterp', [totalBookCount, bookFileCount])}
|
||||
kind={getBookCountKind(monitored, bookFileCount, bookCount)}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
{
|
||||
<span>{bookFileCount} / {bookCount}</span>
|
||||
}
|
||||
</Label>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -235,12 +240,16 @@ BookRow.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool,
|
||||
authorMonitored: PropTypes.bool.isRequired,
|
||||
bookFiles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMonitorBookPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
BookRow.defaultProps = {
|
||||
statistics: {
|
||||
bookCount: 0,
|
||||
bookFileCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default BookRow;
|
||||
|
||||
@@ -2,38 +2,17 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createBookFileSelector from 'Store/Selectors/createBookFileSelector';
|
||||
import BookRow from './BookRow';
|
||||
|
||||
const selectBookFiles = createSelector(
|
||||
(state) => state.bookFiles,
|
||||
(bookFiles) => {
|
||||
const {
|
||||
items
|
||||
} = bookFiles;
|
||||
|
||||
const bookFileDict = items.reduce((acc, file) => {
|
||||
const bookId = file.bookId;
|
||||
if (!acc.hasOwnProperty(bookId)) {
|
||||
acc[bookId] = [];
|
||||
}
|
||||
|
||||
acc[bookId].push(file);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return bookFileDict;
|
||||
}
|
||||
);
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createAuthorSelector(),
|
||||
selectBookFiles,
|
||||
(state, { id }) => id,
|
||||
(author = {}, bookFiles, bookId) => {
|
||||
createBookFileSelector(),
|
||||
(author = {}, bookFile) => {
|
||||
return {
|
||||
authorMonitored: author.monitored,
|
||||
bookFiles: bookFiles[bookId] ?? []
|
||||
bookFilePath: bookFile ? bookFile.path : null
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './BookStatus.css';
|
||||
|
||||
function BookStatus(props) {
|
||||
const {
|
||||
isAvailable,
|
||||
monitored,
|
||||
bookFile
|
||||
} = props;
|
||||
|
||||
const hasBookFile = !!bookFile;
|
||||
|
||||
if (hasBookFile) {
|
||||
const quality = bookFile.quality;
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<BookQuality
|
||||
title={quality.quality.name}
|
||||
size={bookFile.size}
|
||||
quality={quality}
|
||||
isMonitored={monitored}
|
||||
isCutoffNotMet={bookFile.qualityCutoffNotMet}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!monitored) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Label
|
||||
title={translate('NotMonitored')}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{translate('NotMonitored')}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAvailable) {
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Label
|
||||
title={translate('BookAvailableButMissing')}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{translate('Missing')}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
<Label
|
||||
title={translate('NotAvailable')}
|
||||
kind={kinds.INFO}
|
||||
>
|
||||
{translate('NotAvailable')}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BookStatus.propTypes = {
|
||||
isAvailable: PropTypes.bool,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
bookFile: PropTypes.object
|
||||
};
|
||||
|
||||
export default BookStatus;
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import AuthorMetadataProfilePopoverContent from 'AddAuthor/AuthorMetadataProfilePopoverContent';
|
||||
import AuthorMonitorNewItemsOptionsPopoverContent from 'AddAuthor/AuthorMonitorNewItemsOptionsPopoverContent';
|
||||
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
@@ -74,7 +73,6 @@ class EditAuthorModalContent extends Component {
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
path,
|
||||
@@ -103,31 +101,6 @@ class EditAuthorModalContent extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('MonitorNewItems')}
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
className={styles.labelIcon}
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={translate('MonitorNewItems')}
|
||||
body={<AuthorMonitorNewItemsOptionsPopoverContent />}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.MONITOR_NEW_ITEMS_SELECT}
|
||||
name="monitorNewItems"
|
||||
helpText={translate('MonitorNewItemsHelpText')}
|
||||
{...monitorNewItems}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('QualityProfile')}
|
||||
|
||||
@@ -39,7 +39,6 @@ function createMapStateToProps() {
|
||||
|
||||
const authorSettings = _.pick(author, [
|
||||
'monitored',
|
||||
'monitorNewItems',
|
||||
'qualityProfileId',
|
||||
'metadataProfileId',
|
||||
'path',
|
||||
|
||||
280
frontend/src/Author/Editor/AuthorEditor.js
Normal file
280
frontend/src/Author/Editor/AuthorEditor.js
Normal 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;
|
||||
97
frontend/src/Author/Editor/AuthorEditorConnector.js
Normal file
97
frontend/src/Author/Editor/AuthorEditorConnector.js
Normal file
@@ -0,0 +1,97 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { saveAuthorEditor, setAuthorEditorFilter, setAuthorEditorSort, setAuthorEditorTableOption } from 'Store/Actions/authorEditorActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import AuthorEditor from './AuthorEditor';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('authors', 'authorEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
(author, isOrganizingAuthor, isRetaggingAuthor) => {
|
||||
return {
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
...author
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetAuthorEditorSort: setAuthorEditorSort,
|
||||
dispatchSetAuthorEditorFilter: setAuthorEditorFilter,
|
||||
dispatchSetAuthorEditorTableOption: setAuthorEditorTableOption,
|
||||
dispatchSaveAuthorEditor: saveAuthorEditor,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class AuthorEditorConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.dispatchSetAuthorEditorSort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.dispatchSetAuthorEditorFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.dispatchSetAuthorEditorTableOption(payload);
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveAuthorEditor(payload);
|
||||
}
|
||||
|
||||
onMoveSelected = (payload) => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.MOVE_AUTHOR,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<AuthorEditor
|
||||
{...this.props}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthorEditorConnector.propTypes = {
|
||||
dispatchSetAuthorEditorSort: PropTypes.func.isRequired,
|
||||
dispatchSetAuthorEditorFilter: PropTypes.func.isRequired,
|
||||
dispatchSetAuthorEditorTableOption: PropTypes.func.isRequired,
|
||||
dispatchSaveAuthorEditor: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthorEditorConnector);
|
||||
@@ -1,24 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
import { setBookFilter } from 'Store/Actions/bookIndexActions';
|
||||
import { setAuthorEditorFilter } from 'Store/Actions/authorEditorActions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.books.items,
|
||||
(state) => state.bookIndex.filterBuilderProps,
|
||||
(state) => state.authors.items,
|
||||
(state) => state.authorEditor.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'bookIndex'
|
||||
customFilterType: 'authorEditor'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setBookFilter
|
||||
dispatchSetFilter: setAuthorEditorFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
||||
@@ -1,23 +1,11 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.dropdownContainer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
flex: 1;
|
||||
margin-right: 20px;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@@ -36,14 +24,12 @@
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.deleteSelectedButton {
|
||||
composes: button from '~Components/Link/SpinnerButton.css';
|
||||
|
||||
margin-bottom: 10px;
|
||||
margin-left: 50px;
|
||||
height: 35px;
|
||||
}
|
||||
@@ -62,10 +48,6 @@
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.dropdownContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.inputContainer {
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -79,7 +61,6 @@
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: block;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import MoveAuthorModal from 'Author/MoveAuthor/MoveAuthorModal';
|
||||
import MetadataProfileSelectInputConnector from 'Components/Form/MetadataProfileSelectInputConnector';
|
||||
import MonitorNewItemsSelectInput from 'Components/Form/MonitorNewItemsSelectInput';
|
||||
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
@@ -27,7 +26,6 @@ class AuthorEditorFooter extends Component {
|
||||
|
||||
this.state = {
|
||||
monitored: NO_CHANGE,
|
||||
monitorNewItems: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
metadataProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
@@ -48,7 +46,6 @@ class AuthorEditorFooter extends Component {
|
||||
if (prevProps.isSaving && !isSaving && !saveError) {
|
||||
this.setState({
|
||||
monitored: NO_CHANGE,
|
||||
monitorNewItems: NO_CHANGE,
|
||||
qualityProfileId: NO_CHANGE,
|
||||
metadataProfileId: NO_CHANGE,
|
||||
rootFolderPath: NO_CHANGE,
|
||||
@@ -142,13 +139,13 @@ class AuthorEditorFooter extends Component {
|
||||
isDeleting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
columns,
|
||||
onOrganizeAuthorPress,
|
||||
onRetagAuthorPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
monitored,
|
||||
monitorNewItems,
|
||||
qualityProfileId,
|
||||
metadataProfileId,
|
||||
rootFolderPath,
|
||||
@@ -167,99 +164,112 @@ class AuthorEditorFooter extends Component {
|
||||
|
||||
return (
|
||||
<PageContentFooter>
|
||||
<div className={styles.footer}>
|
||||
<div className={styles.dropdownContainer}>
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MonitorAuthor')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MonitorAuthor')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
<SelectInput
|
||||
name="monitored"
|
||||
value={monitored}
|
||||
values={monitoredOptions}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MonitorNewItems')}
|
||||
isSaving={isSaving && monitored !== NO_CHANGE}
|
||||
/>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
<MonitorNewItemsSelectInput
|
||||
name="monitorNewItems"
|
||||
value={monitorNewItems}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
if (name === 'qualityProfileId') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('QualityProfile')}
|
||||
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
<QualityProfileSelectInputConnector
|
||||
name="qualityProfileId"
|
||||
value={qualityProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MetadataProfile')}
|
||||
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
|
||||
/>
|
||||
if (name === 'metadataProfileId') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('MetadataProfile')}
|
||||
isSaving={isSaving && metadataProfileId !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<MetadataProfileSelectInputConnector
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
includeNoChange={true}
|
||||
includeNone={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
<MetadataProfileSelectInputConnector
|
||||
name="metadataProfileId"
|
||||
value={metadataProfileId}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<div
|
||||
key={name}
|
||||
className={styles.inputContainer}
|
||||
>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('RootFolder')}
|
||||
isSaving={isSaving && rootFolderPath !== NO_CHANGE}
|
||||
/>
|
||||
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<RootFolderSelectInputConnector
|
||||
name="rootFolderPath"
|
||||
value={rootFolderPath}
|
||||
includeNoChange={true}
|
||||
isDisabled={!selectedCount}
|
||||
selectedValueOptions={{ includeFreeSpace: false }}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
<div className={styles.buttonContainerContent}>
|
||||
<AuthorEditorFooterLabel
|
||||
label={translate('SelectedCountAuthorsSelectedInterp', [selectedCount])}
|
||||
isSaving={false}
|
||||
/>
|
||||
return null;
|
||||
})
|
||||
}
|
||||
|
||||
<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
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
@@ -267,7 +277,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onOrganizeAuthorPress}
|
||||
>
|
||||
{translate('RenameFiles')}
|
||||
Rename Files
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -277,7 +287,7 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={onRetagAuthorPress}
|
||||
>
|
||||
{translate('WriteMetadataTags')}
|
||||
Write Metadata Tags
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
@@ -286,20 +296,19 @@ class AuthorEditorFooter extends Component {
|
||||
isDisabled={!selectedCount || isOrganizingAuthor || isRetaggingAuthor}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
{translate('SetReadarrTags')}
|
||||
Set Readarr Tags
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.deleteSelectedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
|
||||
</div>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.deleteSelectedButton}
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!selectedCount || isDeleting}
|
||||
onPress={this.onDeleteSelectedPress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,6 +348,7 @@ AuthorEditorFooter.propTypes = {
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired,
|
||||
onOrganizeAuthorPress: PropTypes.func.isRequired,
|
||||
onRetagAuthorPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { bulkDeleteAuthor } from 'Store/Actions/authorIndexActions';
|
||||
import { bulkDeleteAuthor } from 'Store/Actions/authorEditorActions';
|
||||
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
|
||||
import DeleteAuthorModalContent from './DeleteAuthorModalContent';
|
||||
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
@@ -18,9 +15,6 @@ import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
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 AuthorIndexFilterMenu from './Menus/AuthorIndexFilterMenu';
|
||||
import AuthorIndexSortMenu from './Menus/AuthorIndexSortMenu';
|
||||
@@ -58,20 +52,12 @@ class AuthorIndex extends Component {
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isPosterOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false,
|
||||
isEditorActive: false,
|
||||
isOrganizingAuthorModalOpen: false,
|
||||
isRetaggingAuthorModalOpen: false,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
isOverviewOptionsModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
@@ -86,7 +72,6 @@ class AuthorIndex extends Component {
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
if (this.state.jumpToCharacter != null) {
|
||||
@@ -101,48 +86,6 @@ class AuthorIndex extends Component {
|
||||
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() {
|
||||
const {
|
||||
items,
|
||||
@@ -206,72 +149,10 @@ class AuthorIndex extends Component {
|
||||
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) => {
|
||||
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
|
||||
|
||||
@@ -291,16 +172,11 @@ class AuthorIndex extends Component {
|
||||
view,
|
||||
isRefreshingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
onViewSelect,
|
||||
onRefreshAuthorPress,
|
||||
onRssSyncPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -310,31 +186,23 @@ class AuthorIndex extends Component {
|
||||
jumpBarItems,
|
||||
jumpToCharacter,
|
||||
isPosterOptionsModalOpen,
|
||||
isOverviewOptionsModalOpen,
|
||||
isEditorActive,
|
||||
selectedState,
|
||||
allSelected,
|
||||
allUnselected
|
||||
isOverviewOptionsModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedAuthorIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
const hasNoAuthor = !totalItems;
|
||||
|
||||
const refreshLabel = isEditorActive && selectedAuthorIds.length > 0 ? translate('UpdateSelected') : translate('UpdateAll');
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={refreshLabel}
|
||||
label={translate('UpdateAll')}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isRefreshingAuthor}
|
||||
onPress={this.onRefreshAuthorPress}
|
||||
onPress={onRefreshAuthorPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
@@ -345,35 +213,6 @@ class AuthorIndex extends Component {
|
||||
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
|
||||
@@ -471,12 +310,6 @@ class AuthorIndex extends Component {
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
isEditorActive={isEditorActive}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
selectedState={selectedState}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
@@ -499,24 +332,6 @@ class AuthorIndex extends Component {
|
||||
}
|
||||
</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
|
||||
isOpen={isPosterOptionsModalOpen}
|
||||
onModalClose={this.onPosterOptionsModalClose}
|
||||
@@ -525,20 +340,8 @@ class AuthorIndex extends Component {
|
||||
<AuthorIndexOverviewOptionsModal
|
||||
isOpen={isOverviewOptionsModalOpen}
|
||||
onModalClose={this.onOverviewOptionsModalClose}
|
||||
/>
|
||||
|
||||
<OrganizeAuthorModal
|
||||
isOpen={this.state.isOrganizingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onOrganizeAuthorModalClose}
|
||||
/>
|
||||
|
||||
<RetagAuthorModal
|
||||
isOpen={this.state.isRetaggingAuthorModalOpen}
|
||||
authorIds={selectedAuthorIds}
|
||||
onModalClose={this.onRetagAuthorModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -558,21 +361,14 @@ AuthorIndex.propTypes = {
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
view: PropTypes.string.isRequired,
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isOrganizingAuthor: PropTypes.bool.isRequired,
|
||||
isRetaggingAuthor: PropTypes.bool.isRequired,
|
||||
isRssSyncExecuting: 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,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
onScroll: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndex;
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
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 scrollPositions from 'Store/scrollPositions';
|
||||
import createAuthorClientSideCollectionItemsSelector from 'Store/Selectors/createAuthorClientSideCollectionItemsSelector';
|
||||
@@ -18,22 +18,16 @@ function createMapStateToProps() {
|
||||
createAuthorClientSideCollectionItemsSelector('authorIndex'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createCommandExecutingSelector(commandNames.RENAME_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.RETAG_AUTHOR),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
author,
|
||||
isRefreshingAuthor,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
dimensionsState
|
||||
) => {
|
||||
return {
|
||||
...author,
|
||||
isRefreshingAuthor,
|
||||
isOrganizingAuthor,
|
||||
isRetaggingAuthor,
|
||||
isRssSyncExecuting,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
@@ -59,14 +53,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(setAuthorView({ view }));
|
||||
},
|
||||
|
||||
dispatchSaveAuthorEditor(payload) {
|
||||
dispatch(saveAuthorEditor(payload));
|
||||
},
|
||||
|
||||
onRefreshAuthorPress(items) {
|
||||
onRefreshAuthorPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.BULK_REFRESH_AUTHOR,
|
||||
authorIds: items
|
||||
name: commandNames.REFRESH_AUTHOR
|
||||
}));
|
||||
},
|
||||
|
||||
@@ -87,10 +76,6 @@ class AuthorIndexConnector extends Component {
|
||||
this.props.dispatchSetAuthorView(view);
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveAuthorEditor(payload);
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
scrollPositions.authorIndex = scrollTop;
|
||||
}
|
||||
@@ -104,7 +89,6 @@ class AuthorIndexConnector extends Component {
|
||||
{...this.props}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -113,8 +97,7 @@ class AuthorIndexConnector extends Component {
|
||||
AuthorIndexConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
dispatchSetAuthorView: PropTypes.func.isRequired,
|
||||
dispatchSaveAuthorEditor: PropTypes.func.isRequired
|
||||
dispatchSetAuthorView: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
|
||||
@@ -19,13 +19,6 @@ $hoverScale: 1.05;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.posterContainer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -5,7 +5,6 @@ import AuthorPoster from 'Author/AuthorPoster';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
@@ -68,15 +67,6 @@ class AuthorIndexOverview extends Component {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -107,8 +97,6 @@ class AuthorIndexOverview extends Component {
|
||||
isSearchingAuthor,
|
||||
onRefreshAuthorPress,
|
||||
onSearchPress,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -139,18 +127,6 @@ class AuthorIndexOverview extends Component {
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
isEditorActive &&
|
||||
<div className={styles.editorSelect}>
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
@@ -294,10 +270,7 @@ AuthorIndexOverview.propTypes = {
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isSearchingAuthor: PropTypes.bool.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorIndexOverview.defaultProps = {
|
||||
|
||||
@@ -73,9 +73,7 @@ class AuthorIndexOverviews extends Component {
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter,
|
||||
scrollTop,
|
||||
isEditorActive,
|
||||
selectedState
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -93,8 +91,6 @@ class AuthorIndexOverviews extends Component {
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||
prevProps.isEditorActive !== isEditorActive ||
|
||||
prevProps.selectedState !== selectedState ||
|
||||
prevProps.overviewOptions.showTitle !== overviewOptions.showTitle)) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
@@ -152,10 +148,7 @@ class AuthorIndexOverviews extends Component {
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
isSmallScreen,
|
||||
selectedState,
|
||||
isEditorActive,
|
||||
onSelectedChange
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -191,9 +184,6 @@ class AuthorIndexOverviews extends Component {
|
||||
authorId={author.id}
|
||||
qualityProfileId={author.qualityProfileId}
|
||||
metadataProfileId={author.metadataProfileId}
|
||||
isSelected={selectedState[author.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -274,10 +264,7 @@ AuthorIndexOverviews.propTypes = {
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndexOverviews;
|
||||
|
||||
@@ -78,13 +78,6 @@ $hoverScale: 1.05;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
|
||||
@@ -4,7 +4,6 @@ import AuthorPoster from 'Author/AuthorPoster';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import AuthorIndexProgressBar from 'Author/Index/ProgressBar/AuthorIndexProgressBar';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
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
|
||||
|
||||
@@ -94,7 +84,6 @@ class AuthorIndexPoster extends Component {
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
qualityProfile,
|
||||
metadataProfile,
|
||||
showSearchAction,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
@@ -103,9 +92,6 @@ class AuthorIndexPoster extends Component {
|
||||
isSearchingAuthor,
|
||||
onRefreshAuthorPress,
|
||||
onSearchPress,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
onSelectedChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -134,18 +120,6 @@ class AuthorIndexPoster extends Component {
|
||||
<div>
|
||||
<div className={styles.content}>
|
||||
<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}>
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
@@ -260,7 +234,6 @@ class AuthorIndexPoster extends Component {
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
qualityProfile={qualityProfile}
|
||||
showQualityProfile={showQualityProfile}
|
||||
metadataProfile={metadataProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
@@ -302,7 +275,6 @@ AuthorIndexPoster.propTypes = {
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
@@ -310,10 +282,7 @@ AuthorIndexPoster.propTypes = {
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isSearchingAuthor: PropTypes.bool.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorIndexPoster.defaultProps = {
|
||||
|
||||
@@ -8,10 +8,8 @@ function AuthorIndexPosterInfo(props) {
|
||||
const {
|
||||
qualityProfile,
|
||||
showQualityProfile,
|
||||
metadataProfile,
|
||||
previousAiring,
|
||||
added,
|
||||
nextBook,
|
||||
lastBook,
|
||||
bookCount,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
@@ -29,10 +27,20 @@ function AuthorIndexPosterInfo(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (sortKey === 'metadataProfileId') {
|
||||
if (sortKey === 'previousAiring' && previousAiring) {
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{metadataProfile.name}
|
||||
{
|
||||
getRelativeDate(
|
||||
previousAiring,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
}
|
||||
</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') {
|
||||
let books = '1 book';
|
||||
|
||||
@@ -129,10 +101,8 @@ function AuthorIndexPosterInfo(props) {
|
||||
AuthorIndexPosterInfo.propTypes = {
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
metadataProfile: PropTypes.object.isRequired,
|
||||
previousAiring: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
nextBook: PropTypes.object,
|
||||
lastBook: PropTypes.object,
|
||||
bookCount: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
sizeOnDisk: PropTypes.number,
|
||||
|
||||
@@ -116,9 +116,7 @@ class AuthorIndexPosters extends Component {
|
||||
posterOptions,
|
||||
jumpToCharacter,
|
||||
isSmallScreen,
|
||||
isEditorActive,
|
||||
scrollTop,
|
||||
selectedState
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -140,8 +138,6 @@ class AuthorIndexPosters extends Component {
|
||||
prevState.columnCount !== columnCount ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)) ||
|
||||
prevProps.isEditorActive !== isEditorActive ||
|
||||
prevProps.selectedState !== selectedState ||
|
||||
prevProps.posterOptions.showTitle !== posterOptions.showTitle) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
@@ -202,10 +198,7 @@ class AuthorIndexPosters extends Component {
|
||||
posterOptions,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
timeFormat,
|
||||
selectedState,
|
||||
isEditorActive,
|
||||
onSelectedChange
|
||||
timeFormat
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -253,9 +246,6 @@ class AuthorIndexPosters extends Component {
|
||||
authorId={author.id}
|
||||
qualityProfileId={author.qualityProfileId}
|
||||
metadataProfileId={author.metadataProfileId}
|
||||
isSelected={selectedState[author.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -338,10 +328,7 @@ AuthorIndexPosters.propTypes = {
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndexPosters;
|
||||
|
||||
@@ -5,7 +5,6 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import AuthorIndexTableOptionsConnector from './AuthorIndexTableOptionsConnector';
|
||||
import hasGrowableColumns from './hasGrowableColumns';
|
||||
@@ -16,10 +15,6 @@ function AuthorIndexHeader(props) {
|
||||
showBanners,
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
isEditorActive,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -38,21 +33,6 @@ function AuthorIndexHeader(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'select') {
|
||||
if (isEditorActive) {
|
||||
return (
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
key={name}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
@@ -100,10 +80,6 @@ function AuthorIndexHeader(props) {
|
||||
AuthorIndexHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).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
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import getProgressBarKind from 'Utilities/Author/getProgressBarKind';
|
||||
@@ -102,11 +101,8 @@ class AuthorIndexRow extends Component {
|
||||
columns,
|
||||
isRefreshingAuthor,
|
||||
isSearchingAuthor,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
onRefreshAuthorPress,
|
||||
onSearchPress,
|
||||
onSelectedChange
|
||||
onSearchPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -135,19 +131,6 @@ class AuthorIndexRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isEditorActive && name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.checkInput}
|
||||
id={id}
|
||||
key={name}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'status') {
|
||||
return (
|
||||
<AuthorStatusCell
|
||||
@@ -448,10 +431,7 @@ AuthorIndexRow.propTypes = {
|
||||
isRefreshingAuthor: PropTypes.bool.isRequired,
|
||||
isSearchingAuthor: PropTypes.bool.isRequired,
|
||||
onRefreshAuthorPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
onSearchPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
AuthorIndexRow.defaultProps = {
|
||||
|
||||
@@ -48,9 +48,6 @@ class AuthorIndexTable extends Component {
|
||||
const {
|
||||
items,
|
||||
columns,
|
||||
selectedState,
|
||||
onSelectedChange,
|
||||
isEditorActive,
|
||||
showBanners,
|
||||
showTitle
|
||||
} = this.props;
|
||||
@@ -70,9 +67,6 @@ class AuthorIndexTable extends Component {
|
||||
authorId={author.id}
|
||||
qualityProfileId={author.qualityProfileId}
|
||||
metadataProfileId={author.metadataProfileId}
|
||||
isSelected={selectedState[author.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
isEditorActive={isEditorActive}
|
||||
showBanners={showBanners}
|
||||
showTitle={showTitle}
|
||||
/>
|
||||
@@ -93,12 +87,7 @@ class AuthorIndexTable extends Component {
|
||||
isSmallScreen,
|
||||
onSortPress,
|
||||
scroller,
|
||||
scrollTop,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
isEditorActive,
|
||||
selectedState
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -119,13 +108,8 @@ class AuthorIndexTable extends Component {
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
isEditorActive={isEditorActive}
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
@@ -145,13 +129,7 @@ AuthorIndexTable.propTypes = {
|
||||
scrollTop: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
isSmallScreen: PropTypes.bool.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
|
||||
onSortPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthorIndexTable;
|
||||
|
||||
@@ -14,7 +14,7 @@ class MonitoringOptionsModalConnector extends Component {
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearPendingChanges({ section: 'authors' });
|
||||
this.props.clearPendingChanges({ section: 'series' });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
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 ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
const NO_CHANGE = 'noChange';
|
||||
@@ -51,8 +50,7 @@ class MonitoringOptionsModalContent extends Component {
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
onSavePress,
|
||||
isSaving
|
||||
onSavePress
|
||||
} = this.props;
|
||||
const {
|
||||
monitor
|
||||
@@ -61,14 +59,6 @@ class MonitoringOptionsModalContent extends Component {
|
||||
if (monitor !== NO_CHANGE) {
|
||||
onSavePress({ monitor });
|
||||
}
|
||||
|
||||
if (!isSaving) {
|
||||
this.onModalClose();
|
||||
}
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -89,16 +79,10 @@ class MonitoringOptionsModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('MonitorBook')}
|
||||
{translate('MonitorBook')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
{translate('MonitorBookExistingOnlyWarning')}
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Monitoring')}</FormLabel>
|
||||
|
||||
@@ -7,12 +7,12 @@ import MonitoringOptionsModalContent from './MonitoringOptionsModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.authors,
|
||||
(authorState) => {
|
||||
(state) => state.series,
|
||||
(seriesState) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError
|
||||
} = authorState;
|
||||
} = seriesState;
|
||||
|
||||
return {
|
||||
isSaving,
|
||||
|
||||
@@ -5,16 +5,13 @@ import { kinds } from 'Helpers/Props';
|
||||
import styles from './NoAuthor.css';
|
||||
|
||||
function NoAuthor(props) {
|
||||
const {
|
||||
totalItems,
|
||||
itemType
|
||||
} = props;
|
||||
const { totalItems } = props;
|
||||
|
||||
if (totalItems > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
{`All ${itemType} are hidden due to the applied filter.`}
|
||||
All authors are hidden due to the applied filter.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -23,7 +20,7 @@ function NoAuthor(props) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
{`No ${itemType} found, to get started you'll want to add a new author or book or add an existing library location (Root Folder) and update.`}
|
||||
No authors found, to get started you'll want to add a new author or book or add an existing library location (Root Folder) and update.
|
||||
</div>
|
||||
|
||||
<div className={styles.buttonContainer}>
|
||||
@@ -48,12 +45,7 @@ function NoAuthor(props) {
|
||||
}
|
||||
|
||||
NoAuthor.propTypes = {
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
itemType: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
NoAuthor.defaultProps = {
|
||||
itemType: 'authors'
|
||||
totalItems: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default NoAuthor;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
||||
function BookNameLink({ titleSlug, title }) {
|
||||
const link = `/book/${titleSlug}`;
|
||||
|
||||
return (
|
||||
<Link to={link}>
|
||||
{title}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
BookNameLink.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default BookNameLink;
|
||||
@@ -4,7 +4,11 @@ import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
|
||||
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||
function getTooltip(title, quality, size) {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
|
||||
const revision = quality.revision;
|
||||
|
||||
if (revision.real && revision.real > 0) {
|
||||
@@ -19,12 +23,6 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||
title += ` - ${formatBytes(size)}`;
|
||||
}
|
||||
|
||||
if (!isMonitored) {
|
||||
title += ' [Not Monitored]';
|
||||
} else if (isCutoffNotMet) {
|
||||
title += ' [Cutoff Not Met]';
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -34,26 +32,14 @@ function BookQuality(props) {
|
||||
title,
|
||||
quality,
|
||||
size,
|
||||
isMonitored,
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
if (!isMonitored) {
|
||||
kind = kinds.DISABLED;
|
||||
} else if (isCutoffNotMet) {
|
||||
kind = kinds.INVERSE;
|
||||
}
|
||||
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kind}
|
||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||
title={getTooltip(title, quality, size)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>
|
||||
@@ -65,13 +51,11 @@ BookQuality.propTypes = {
|
||||
title: PropTypes.string,
|
||||
quality: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
isMonitored: PropTypes.bool,
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
};
|
||||
|
||||
BookQuality.defaultProps = {
|
||||
title: '',
|
||||
isMonitored: true
|
||||
title: ''
|
||||
};
|
||||
|
||||
export default BookQuality;
|
||||
|
||||
@@ -8,7 +8,6 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import { toggleBooksMonitored } from 'Store/Actions/bookActions';
|
||||
import { clearBookFiles, fetchBookFiles } from 'Store/Actions/bookFileActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
|
||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
@@ -44,17 +43,15 @@ function createMapStateToProps() {
|
||||
(state, { titleSlug }) => titleSlug,
|
||||
selectBookFiles,
|
||||
(state) => state.books,
|
||||
(state) => state.editions,
|
||||
createAllAuthorSelector(),
|
||||
createCommandsSelector(),
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(titleSlug, bookFiles, books, editions, authors, commands, uiSettings, dimensions) => {
|
||||
const book = books.items.find((b) => b.titleSlug === titleSlug);
|
||||
const author = authors.find((a) => a.id === book.authorId);
|
||||
const sortedBooks = books.items.filter((b) => b.authorId === book.authorId);
|
||||
sortedBooks.sort((a, b) => ((a.releaseDate > b.releaseDate) ? 1 : -1));
|
||||
const bookIndex = sortedBooks.findIndex((b) => b.id === book.id);
|
||||
(titleSlug, bookFiles, books, authors, commands, uiSettings, dimensions) => {
|
||||
const sortedBooks = _.orderBy(books.items, 'releaseDate');
|
||||
const bookIndex = _.findIndex(sortedBooks, { titleSlug });
|
||||
const book = sortedBooks[bookIndex];
|
||||
const author = _.find(authors, { id: book.authorId });
|
||||
|
||||
if (!book) {
|
||||
return {};
|
||||
@@ -81,8 +78,8 @@ function createMapStateToProps() {
|
||||
isRefreshingCommand.body.bookId === book.id
|
||||
);
|
||||
|
||||
const isFetching = isBookFilesFetching || editions.isFetching;
|
||||
const isPopulated = isBookFilesPopulated && editions.isPopulated;
|
||||
const isFetching = isBookFilesFetching;
|
||||
const isPopulated = isBookFilesPopulated;
|
||||
|
||||
return {
|
||||
...book,
|
||||
@@ -106,8 +103,6 @@ const mapDispatchToProps = {
|
||||
executeCommand,
|
||||
fetchBookFiles,
|
||||
clearBookFiles,
|
||||
fetchEditions,
|
||||
clearEditions,
|
||||
clearReleases,
|
||||
cancelFetchReleases,
|
||||
toggleBooksMonitored
|
||||
@@ -125,8 +120,7 @@ class BookDetailsConnector extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.id !== this.props.id ||
|
||||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||
if (!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
@@ -145,14 +139,12 @@ class BookDetailsConnector extends Component {
|
||||
const bookId = this.props.id;
|
||||
|
||||
this.props.fetchBookFiles({ bookId });
|
||||
this.props.fetchEditions({ bookId });
|
||||
}
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.cancelFetchReleases();
|
||||
this.props.clearReleases();
|
||||
this.props.clearBookFiles();
|
||||
this.props.clearEditions();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -202,8 +194,6 @@ BookDetailsConnector.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
fetchBookFiles: PropTypes.func.isRequired,
|
||||
clearBookFiles: PropTypes.func.isRequired,
|
||||
fetchEditions: PropTypes.func.isRequired,
|
||||
clearEditions: PropTypes.func.isRequired,
|
||||
clearReleases: PropTypes.func.isRequired,
|
||||
cancelFetchReleases: PropTypes.func.isRequired,
|
||||
toggleBooksMonitored: PropTypes.func.isRequired,
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
|
||||
.duration {
|
||||
margin-right: 15px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.detailsLabel {
|
||||
|
||||
@@ -133,7 +133,6 @@ class BookDetailsHeader extends Component {
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{author.authorName}
|
||||
{
|
||||
!!pageCount &&
|
||||
<span className={styles.duration}>
|
||||
@@ -149,24 +148,21 @@ class BookDetailsHeader extends Component {
|
||||
</div>
|
||||
|
||||
<div className={styles.detailsLabels}>
|
||||
{
|
||||
releaseDate &&
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CALENDAR}
|
||||
size={17}
|
||||
/>
|
||||
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
moment(releaseDate).format(shortDateFormat)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
moment(releaseDate).format(shortDateFormat)
|
||||
}
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<Label
|
||||
className={styles.detailsLabel}
|
||||
|
||||
@@ -8,25 +8,15 @@ import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
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() {
|
||||
return createSelector(
|
||||
createBookSelector(),
|
||||
selectOverview,
|
||||
createUISettingsSelector(),
|
||||
createDimensionsSelector(),
|
||||
(book, overview, uiSettings, dimensions) => {
|
||||
(book, uiSettings, dimensions) => {
|
||||
|
||||
return {
|
||||
...book,
|
||||
overview,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import NotFound from 'Components/NotFound';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import { clearBooks, fetchBooks } from 'Store/Actions/bookActions';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookDetailsConnector from './BookDetailsConnector';
|
||||
|
||||
@@ -43,7 +44,9 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
push
|
||||
push,
|
||||
fetchBooks,
|
||||
clearBooks
|
||||
};
|
||||
|
||||
class BookDetailsPageConnector extends Component {
|
||||
@@ -59,11 +62,24 @@ class BookDetailsPageConnector extends Component {
|
||||
this.populate();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.unpopulate();
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
populate = () => {
|
||||
const titleSlug = this.props.titleSlug;
|
||||
this.setState({ hasMounted: true });
|
||||
this.props.fetchBooks({
|
||||
titleSlug,
|
||||
includeAllAuthorBooks: true
|
||||
});
|
||||
}
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.clearBooks();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -109,6 +125,8 @@ BookDetailsPageConnector.propTypes = {
|
||||
titleSlug: PropTypes.string,
|
||||
match: PropTypes.shape({ params: PropTypes.shape({ titleSlug: PropTypes.string.isRequired }).isRequired }).isRequired,
|
||||
push: PropTypes.func.isRequired,
|
||||
fetchBooks: PropTypes.func.isRequired,
|
||||
clearBooks: PropTypes.func.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
@@ -6,13 +6,11 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
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 } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class EditBookModalContent extends Component {
|
||||
@@ -38,9 +36,6 @@ class EditBookModalContent extends Component {
|
||||
authorName,
|
||||
statistics,
|
||||
item,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
onInputChange,
|
||||
onModalClose,
|
||||
@@ -53,8 +48,7 @@ class EditBookModalContent extends Component {
|
||||
editions
|
||||
} = item;
|
||||
|
||||
const hasFile = statistics ? statistics.bookFileCount > 0 : false;
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load editions');
|
||||
const hasFile = statistics ? statistics.bookFileCount : 0;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -94,33 +88,20 @@ class EditBookModalContent extends Component {
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Edition')}
|
||||
</FormLabel>
|
||||
|
||||
{
|
||||
error &&
|
||||
<div>{errorMessage}</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !isFetching && !!editions.value.length &&
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('Edition')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.BOOK_EDITION_SELECT}
|
||||
name="editions"
|
||||
helpText={translate('EditionsHelpText')}
|
||||
isDisabled={anyEditionOk.value && hasFile}
|
||||
bookEditions={editions}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
<FormInputGroup
|
||||
type={inputTypes.BOOK_EDITION_SELECT}
|
||||
name="editions"
|
||||
helpText={translate('EditionsHelpText')}
|
||||
isDisabled={anyEditionOk.value && hasFile}
|
||||
bookEditions={editions}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</Form>
|
||||
</ModalBody>
|
||||
@@ -150,9 +131,6 @@ EditBookModalContent.propTypes = {
|
||||
authorName: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
item: PropTypes.object.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveBook, setBookValue } from 'Store/Actions/bookActions';
|
||||
import { saveEditions } from 'Store/Actions/editionActions';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import createBookSelector from 'Store/Selectors/createBookSelector';
|
||||
import selectSettings from 'Store/Selectors/selectSettings';
|
||||
@@ -13,27 +12,20 @@ import EditBookModalContent from './EditBookModalContent';
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.books,
|
||||
(state) => state.editions,
|
||||
createBookSelector(),
|
||||
createAuthorSelector(),
|
||||
(bookState, editionState, book, author) => {
|
||||
(bookState, book, author) => {
|
||||
const {
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges
|
||||
} = bookState;
|
||||
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error
|
||||
} = editionState;
|
||||
|
||||
const bookSettings = _.pick(book, [
|
||||
'monitored',
|
||||
'anyEditionOk'
|
||||
'anyEditionOk',
|
||||
'editions'
|
||||
]);
|
||||
bookSettings.editions = editionState.items;
|
||||
|
||||
const settings = selectSettings(bookSettings, pendingChanges, saveError);
|
||||
|
||||
@@ -42,9 +34,6 @@ function createMapStateToProps() {
|
||||
authorName: author.authorName,
|
||||
bookType: book.bookType,
|
||||
statistics: book.statistics,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
saveError,
|
||||
item: settings.settings,
|
||||
@@ -56,8 +45,7 @@ function createMapStateToProps() {
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetBookValue: setBookValue,
|
||||
dispatchSaveBook: saveBook,
|
||||
dispatchSaveEditions: saveEditions
|
||||
dispatchSaveBook: saveBook
|
||||
};
|
||||
|
||||
class EditBookModalContentConnector extends Component {
|
||||
@@ -82,9 +70,6 @@ class EditBookModalContentConnector extends Component {
|
||||
this.props.dispatchSaveBook({
|
||||
id: this.props.bookId
|
||||
});
|
||||
this.props.dispatchSaveEditions({
|
||||
id: this.props.bookId
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
@@ -107,7 +92,6 @@ EditBookModalContentConnector.propTypes = {
|
||||
saveError: PropTypes.object,
|
||||
dispatchSetBookValue: PropTypes.func.isRequired,
|
||||
dispatchSaveBook: PropTypes.func.isRequired,
|
||||
dispatchSaveEditions: 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);
|
||||
@@ -1,72 +0,0 @@
|
||||
.pageContentBodyWrapper {
|
||||
display: flex;
|
||||
flex: 1 0 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
margin-top: 20px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.contentBody {
|
||||
composes: contentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.postersInnerContentBody {
|
||||
composes: innerContentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
/* 5px less padding than normal to handle poster's 5px margin */
|
||||
padding: calc($pageContentBodyPadding - 5px);
|
||||
}
|
||||
|
||||
.bannersInnerContentBody {
|
||||
composes: innerContentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
/* 5px less padding than normal to handle poster's 5px margin */
|
||||
padding: calc($pageContentBodyPadding - 5px);
|
||||
}
|
||||
|
||||
.tableInnerContentBody {
|
||||
composes: innerContentBody from '~Components/Page/PageContentBody.css';
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.contentBodyContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.pageContentBodyWrapper {
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.contentBody {
|
||||
flex-basis: 1px;
|
||||
}
|
||||
|
||||
.postersInnerContentBody {
|
||||
padding: calc($pageContentBodyPaddingSmallScreen - 5px);
|
||||
}
|
||||
|
||||
.bannersInnerContentBody {
|
||||
padding: calc($pageContentBodyPaddingSmallScreen - 5px);
|
||||
}
|
||||
}
|
||||
@@ -1,586 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import NoAuthor from 'Author/NoAuthor';
|
||||
import BookEditorFooter from 'Book/Editor/BookEditorFooter';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
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 TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
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 BookIndexFilterMenu from './Menus/BookIndexFilterMenu';
|
||||
import BookIndexSortMenu from './Menus/BookIndexSortMenu';
|
||||
import BookIndexViewMenu from './Menus/BookIndexViewMenu';
|
||||
import BookIndexOverviewsConnector from './Overview/BookIndexOverviewsConnector';
|
||||
import BookIndexOverviewOptionsModal from './Overview/Options/BookIndexOverviewOptionsModal';
|
||||
import BookIndexPostersConnector from './Posters/BookIndexPostersConnector';
|
||||
import BookIndexPosterOptionsModal from './Posters/Options/BookIndexPosterOptionsModal';
|
||||
import BookIndexTableConnector from './Table/BookIndexTableConnector';
|
||||
import BookIndexTableOptionsConnector from './Table/BookIndexTableOptionsConnector';
|
||||
import styles from './BookIndex.css';
|
||||
|
||||
function getViewComponent(view) {
|
||||
if (view === 'posters') {
|
||||
return BookIndexPostersConnector;
|
||||
}
|
||||
|
||||
if (view === 'overview') {
|
||||
return BookIndexOverviewsConnector;
|
||||
}
|
||||
|
||||
return BookIndexTableConnector;
|
||||
}
|
||||
|
||||
class BookIndex extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
scroller: null,
|
||||
jumpBarItems: { order: [] },
|
||||
jumpToCharacter: null,
|
||||
isPosterOptionsModalOpen: false,
|
||||
isOverviewOptionsModalOpen: false,
|
||||
isConfirmSearchModalOpen: false,
|
||||
isEditorActive: false,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setJumpBarItems();
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
if (this.state.jumpToCharacter != null) {
|
||||
this.setState({ jumpToCharacter: null });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (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() {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isPopulated
|
||||
} = this.props;
|
||||
|
||||
// Reset if not sorting by sortName
|
||||
if (!isPopulated || (sortKey !== 'title' && sortKey !== 'authorTitle')) {
|
||||
this.setState({ jumpBarItems: { order: [] } });
|
||||
return;
|
||||
}
|
||||
|
||||
const characters = _.reduce(items, (acc, item) => {
|
||||
let char = item[sortKey].charAt(0);
|
||||
|
||||
if (!isNaN(char)) {
|
||||
char = '#';
|
||||
}
|
||||
|
||||
if (char in acc) {
|
||||
acc[char] = acc[char] + 1;
|
||||
} else {
|
||||
acc[char] = 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const order = Object.keys(characters).sort();
|
||||
|
||||
// Reverse if sorting descending
|
||||
if (sortDirection === sortDirections.DESCENDING) {
|
||||
order.reverse();
|
||||
}
|
||||
|
||||
const jumpBarItems = {
|
||||
characters,
|
||||
order
|
||||
};
|
||||
|
||||
this.setState({ jumpBarItems });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPosterOptionsPress = () => {
|
||||
this.setState({ isPosterOptionsModalOpen: true });
|
||||
}
|
||||
|
||||
onPosterOptionsModalClose = () => {
|
||||
this.setState({ isPosterOptionsModalOpen: false });
|
||||
}
|
||||
|
||||
onOverviewOptionsPress = () => {
|
||||
this.setState({ isOverviewOptionsModalOpen: true });
|
||||
}
|
||||
|
||||
onOverviewOptionsModalClose = () => {
|
||||
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) => {
|
||||
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() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
totalItems,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
view,
|
||||
isRefreshingBook,
|
||||
isRssSyncExecuting,
|
||||
isSearching,
|
||||
isSaving,
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
onScroll,
|
||||
onSortSelect,
|
||||
onFilterSelect,
|
||||
onViewSelect,
|
||||
onRssSyncPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
scroller,
|
||||
jumpBarItems,
|
||||
jumpToCharacter,
|
||||
isPosterOptionsModalOpen,
|
||||
isOverviewOptionsModalOpen,
|
||||
isConfirmSearchModalOpen,
|
||||
isEditorActive,
|
||||
selectedState,
|
||||
allSelected,
|
||||
allUnselected
|
||||
} = this.state;
|
||||
|
||||
const selectedBookIds = this.getSelectedIds();
|
||||
|
||||
const ViewComponent = getViewComponent(view);
|
||||
const isLoaded = !!(!error && isPopulated && items.length && scroller);
|
||||
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 (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={refreshLabel}
|
||||
iconName={icons.REFRESH}
|
||||
spinningName={icons.REFRESH}
|
||||
isSpinning={isRefreshingBook}
|
||||
onPress={this.onRefreshBookPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RSSSync')}
|
||||
iconName={icons.RSS}
|
||||
isSpinning={isRssSyncExecuting}
|
||||
isDisabled={hasNoAuthor}
|
||||
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
|
||||
alignContent={align.RIGHT}
|
||||
collapseButtons={false}
|
||||
>
|
||||
{
|
||||
view === 'table' ?
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
optionsComponent={BookIndexTableOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'posters' ?
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.POSTER}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onPosterOptionsPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'overview' ?
|
||||
<PageToolbarButton
|
||||
label={translate('Options')}
|
||||
iconName={icons.OVERVIEW}
|
||||
isDisabled={hasNoAuthor}
|
||||
onPress={this.onOverviewOptionsPress}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<BookIndexViewMenu
|
||||
view={view}
|
||||
isDisabled={hasNoAuthor}
|
||||
onViewSelect={onViewSelect}
|
||||
/>
|
||||
|
||||
<BookIndexSortMenu
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
isDisabled={hasNoAuthor}
|
||||
onSortSelect={onSortSelect}
|
||||
/>
|
||||
|
||||
<BookIndexFilterMenu
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
isDisabled={hasNoAuthor}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<div className={styles.pageContentBodyWrapper}>
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
className={styles.contentBody}
|
||||
innerClassName={styles[`${view}InnerContentBody`]}
|
||||
onScroll={onScroll}
|
||||
>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div className={styles.errorMessage}>
|
||||
{getErrorMessage(error, 'Failed to load books from API')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isLoaded &&
|
||||
<div className={styles.contentBodyContainer}>
|
||||
<ViewComponent
|
||||
scroller={scroller}
|
||||
items={items}
|
||||
filters={filters}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
jumpToCharacter={jumpToCharacter}
|
||||
isEditorActive={isEditorActive}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
selectedState={selectedState}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
||||
<BookIndexFooterConnector />
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
!error && isPopulated && !items.length &&
|
||||
<NoAuthor
|
||||
totalItems={totalItems}
|
||||
itemType={'books'}
|
||||
/>
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
{
|
||||
isLoaded && !!jumpBarItems.order.length &&
|
||||
<PageJumpBar
|
||||
items={jumpBarItems}
|
||||
onItemPress={this.onJumpBarItemPress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
isLoaded && isEditorActive &&
|
||||
<BookEditorFooter
|
||||
bookIds={selectedBookIds}
|
||||
selectedCount={selectedBookIds.length}
|
||||
isSaving={isSaving}
|
||||
saveError={saveError}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
}
|
||||
|
||||
<BookIndexPosterOptionsModal
|
||||
isOpen={isPosterOptionsModalOpen}
|
||||
onModalClose={this.onPosterOptionsModalClose}
|
||||
/>
|
||||
|
||||
<BookIndexOverviewOptionsModal
|
||||
isOpen={isOverviewOptionsModalOpen}
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndex.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,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
view: PropTypes.string.isRequired,
|
||||
isRefreshingBook: PropTypes.bool.isRequired,
|
||||
isSearching: PropTypes.bool.isRequired,
|
||||
isRssSyncExecuting: 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,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired,
|
||||
onRefreshBookPress: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookIndex;
|
||||
@@ -1,133 +0,0 @@
|
||||
/* eslint max-params: 0 */
|
||||
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 withScrollPosition from 'Components/withScrollPosition';
|
||||
import { saveBookEditor, setBookFilter, setBookSort, setBookTableOption, setBookView } from 'Store/Actions/bookIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import createBookClientSideCollectionItemsSelector from 'Store/Selectors/createBookClientSideCollectionItemsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import BookIndex from './BookIndex';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createBookClientSideCollectionItemsSelector('bookIndex'),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_AUTHOR),
|
||||
createCommandExecutingSelector(commandNames.REFRESH_BOOK),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createCommandExecutingSelector(commandNames.CUTOFF_UNMET_BOOK_SEARCH),
|
||||
createCommandExecutingSelector(commandNames.MISSING_BOOK_SEARCH),
|
||||
createDimensionsSelector(),
|
||||
(
|
||||
book,
|
||||
isRefreshingAuthorCommand,
|
||||
isRefreshingBookCommand,
|
||||
isRssSyncExecuting,
|
||||
isCutoffBooksSearch,
|
||||
isMissingBooksSearch,
|
||||
dimensionsState
|
||||
) => {
|
||||
const isRefreshingBook = isRefreshingBookCommand || isRefreshingAuthorCommand;
|
||||
return {
|
||||
...book,
|
||||
isRefreshingBook,
|
||||
isRssSyncExecuting,
|
||||
isSearching: isCutoffBooksSearch || isMissingBooksSearch,
|
||||
isSmallScreen: dimensionsState.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onTableOptionChange(payload) {
|
||||
dispatch(setBookTableOption(payload));
|
||||
},
|
||||
|
||||
onSortSelect(sortKey) {
|
||||
dispatch(setBookSort({ sortKey }));
|
||||
},
|
||||
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setBookFilter({ selectedFilterKey }));
|
||||
},
|
||||
|
||||
dispatchSetBookView(view) {
|
||||
dispatch(setBookView({ view }));
|
||||
},
|
||||
|
||||
dispatchSaveBookEditor(payload) {
|
||||
dispatch(saveBookEditor(payload));
|
||||
},
|
||||
|
||||
onRefreshBookPress(items) {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.BULK_REFRESH_BOOK,
|
||||
bookIds: items
|
||||
}));
|
||||
},
|
||||
|
||||
onRssSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RSS_SYNC
|
||||
}));
|
||||
},
|
||||
|
||||
onSearchPress(items) {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.BOOK_SEARCH,
|
||||
bookIds: items
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class BookIndexConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onViewSelect = (view) => {
|
||||
this.props.dispatchSetBookView(view);
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.dispatchSaveBookEditor(payload);
|
||||
}
|
||||
|
||||
onScroll = ({ scrollTop }) => {
|
||||
scrollPositions.bookIndex = scrollTop;
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<BookIndex
|
||||
{...this.props}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexConnector.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
dispatchSetBookView: PropTypes.func.isRequired,
|
||||
dispatchSaveBookEditor: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
connect(createMapStateToProps, createMapDispatchToProps)(BookIndexConnector),
|
||||
'bookIndex'
|
||||
);
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 20px;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
.legendItem {
|
||||
display: flex;
|
||||
margin-bottom: 4px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.legendItemColor {
|
||||
margin-right: 8px;
|
||||
width: 30px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.continuing {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $primaryColor;
|
||||
}
|
||||
|
||||
.ended {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $successColor;
|
||||
}
|
||||
|
||||
.missingMonitored {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $dangerColor;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color($dangerColor shade(5%)), color($dangerColor shade(5%)) 5px, color($dangerColor shade(15%)) 5px, color($dangerColor shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.missingUnmonitored {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $warningColor;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, $warningColor, $warningColor 5px, color($warningColor tint(15%)) 5px, color($warningColor tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.statistics {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpointLarge) {
|
||||
.statistics {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $breakpointSmall) {
|
||||
.footer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.statistics {
|
||||
display: flex;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './BookIndexFooter.css';
|
||||
|
||||
class BookIndexFooter extends PureComponent {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const { book } = this.props;
|
||||
const count = book.length;
|
||||
let books = 0;
|
||||
let bookFiles = 0;
|
||||
let monitored = 0;
|
||||
let totalFileSize = 0;
|
||||
|
||||
const authors = new Set();
|
||||
|
||||
book.forEach((s) => {
|
||||
authors.add(s.authorId);
|
||||
|
||||
const { statistics = {} } = s;
|
||||
|
||||
const {
|
||||
bookCount = 0,
|
||||
bookFileCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
books += bookCount;
|
||||
bookFiles += bookFileCount;
|
||||
|
||||
if (s.monitored) {
|
||||
monitored++;
|
||||
}
|
||||
|
||||
totalFileSize += sizeOnDisk;
|
||||
});
|
||||
|
||||
return (
|
||||
<ColorImpairedConsumer>
|
||||
{(enableColorImpairedMode) => {
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
<div>
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.continuing,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('ContinuingAllBooksDownloaded')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.ended,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('EndedAllBooksDownloaded')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingMonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('MissingBooksAuthorMonitored')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingUnmonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{translate('MissingBooksAuthorNotMonitored')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statistics}>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Monitored')}
|
||||
data={monitored}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Unmonitored')}
|
||||
data={count - monitored}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('Authors')}
|
||||
data={authors.size}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Books')}
|
||||
data={books}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Files')}
|
||||
data={bookFiles}
|
||||
/>
|
||||
</DescriptionList>
|
||||
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title={translate('TotalFileSize')}
|
||||
data={formatBytes(totalFileSize)}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ColorImpairedConsumer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexFooter.propTypes = {
|
||||
book: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default BookIndexFooter;
|
||||
@@ -1,48 +0,0 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createDeepEqualSelector from 'Store/Selectors/createDeepEqualSelector';
|
||||
import BookIndexFooter from './BookIndexFooter';
|
||||
|
||||
function createUnoptimizedSelector() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('books', 'bookIndex'),
|
||||
(books) => {
|
||||
return books.items.map((s) => {
|
||||
const {
|
||||
authorId,
|
||||
monitored,
|
||||
status,
|
||||
statistics
|
||||
} = s;
|
||||
|
||||
return {
|
||||
authorId,
|
||||
monitored,
|
||||
status,
|
||||
statistics
|
||||
};
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createBookSelector() {
|
||||
return createDeepEqualSelector(
|
||||
createUnoptimizedSelector(),
|
||||
(book) => book
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createBookSelector(),
|
||||
(book) => {
|
||||
return {
|
||||
book
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(BookIndexFooter);
|
||||
@@ -1,141 +0,0 @@
|
||||
/* eslint max-params: 0 */
|
||||
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 { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createBookAuthorSelector from 'Store/Selectors/createBookAuthorSelector';
|
||||
import createBookQualityProfileSelector from 'Store/Selectors/createBookQualityProfileSelector';
|
||||
import createBookSelector from 'Store/Selectors/createBookSelector';
|
||||
import createExecutingCommandsSelector from 'Store/Selectors/createExecutingCommandsSelector';
|
||||
|
||||
function selectShowSearchAction() {
|
||||
return createSelector(
|
||||
(state) => state.bookIndex,
|
||||
(bookIndex) => {
|
||||
const view = bookIndex.view;
|
||||
|
||||
switch (view) {
|
||||
case 'posters':
|
||||
return bookIndex.posterOptions.showSearchAction;
|
||||
case 'banners':
|
||||
return bookIndex.bannerOptions.showSearchAction;
|
||||
case 'overview':
|
||||
return bookIndex.overviewOptions.showSearchAction;
|
||||
default:
|
||||
return bookIndex.tableOptions.showSearchAction;
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createBookSelector(),
|
||||
createBookAuthorSelector(),
|
||||
createBookQualityProfileSelector(),
|
||||
selectShowSearchAction(),
|
||||
createExecutingCommandsSelector(),
|
||||
(
|
||||
book,
|
||||
author,
|
||||
qualityProfile,
|
||||
showSearchAction,
|
||||
executingCommands
|
||||
) => {
|
||||
|
||||
// If a book is deleted this selector may fire before the parent
|
||||
// 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
|
||||
// trying to show an book that has no information available.
|
||||
|
||||
if (!book) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const isRefreshingBook = executingCommands.some((command) => {
|
||||
return (
|
||||
(command.name === commandNames.REFRESH_AUTHOR &&
|
||||
command.body.authorId === book.authorId) ||
|
||||
(command.name === commandNames.REFRESH_BOOK &&
|
||||
command.body.bookId === book.id)
|
||||
);
|
||||
});
|
||||
|
||||
const isSearchingBook = executingCommands.some((command) => {
|
||||
return (
|
||||
(command.name === commandNames.AUTHOR_SEARCH &&
|
||||
command.body.authorId === book.authorId) ||
|
||||
(command.name === commandNames.BOOK_SEARCH &&
|
||||
command.body.bookIds.includes(book.id))
|
||||
);
|
||||
});
|
||||
|
||||
return {
|
||||
...book,
|
||||
author,
|
||||
qualityProfile,
|
||||
showSearchAction,
|
||||
isRefreshingBook,
|
||||
isSearchingBook
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class BookIndexItemConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRefreshBookPress = () => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.REFRESH_BOOK,
|
||||
bookId: this.props.id
|
||||
});
|
||||
}
|
||||
|
||||
onSearchPress = () => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.BOOK_SEARCH,
|
||||
bookIds: [this.props.id]
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
component: ItemComponent,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
if (!id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ItemComponent
|
||||
{...otherProps}
|
||||
id={id}
|
||||
onRefreshBookPress={this.onRefreshBookPress}
|
||||
onSearchPress={this.onSearchPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexItemConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
component: PropTypes.elementType.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(BookIndexItemConnector);
|
||||
@@ -1,41 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import BookIndexFilterModalConnector from 'Book/Index/BookIndexFilterModalConnector';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
function BookIndexFilterMenu(props) {
|
||||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
isDisabled,
|
||||
onFilterSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
isDisabled={isDisabled}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={BookIndexFilterModalConnector}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
BookIndexFilterMenu.propTypes = {
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
BookIndexFilterMenu.defaultProps = {
|
||||
showCustomFilters: false
|
||||
};
|
||||
|
||||
export default BookIndexFilterMenu;
|
||||
@@ -1,114 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SortMenu from 'Components/Menu/SortMenu';
|
||||
import SortMenuItem from 'Components/Menu/SortMenuItem';
|
||||
import { align, sortDirections } from 'Helpers/Props';
|
||||
|
||||
function BookIndexSortMenu(props) {
|
||||
const {
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isDisabled,
|
||||
onSortSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<SortMenu
|
||||
isDisabled={isDisabled}
|
||||
alignMenu={align.RIGHT}
|
||||
>
|
||||
<MenuContent>
|
||||
<SortMenuItem
|
||||
name="status"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Monitored/Status
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="title"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Title
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="authorTitle"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Author, Title
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="releaseDate"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Release Date
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="qualityProfileId"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Quality Profile
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="added"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Added
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="bookFileCount"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Files
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="path"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Path
|
||||
</SortMenuItem>
|
||||
|
||||
<SortMenuItem
|
||||
name="sizeOnDisk"
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onPress={onSortSelect}
|
||||
>
|
||||
Size on Disk
|
||||
</SortMenuItem>
|
||||
</MenuContent>
|
||||
</SortMenu>
|
||||
);
|
||||
}
|
||||
|
||||
BookIndexSortMenu.propTypes = {
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onSortSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookIndexSortMenu;
|
||||
@@ -1,55 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import ViewMenu from 'Components/Menu/ViewMenu';
|
||||
import ViewMenuItem from 'Components/Menu/ViewMenuItem';
|
||||
import { align } from 'Helpers/Props';
|
||||
|
||||
function BookIndexViewMenu(props) {
|
||||
const {
|
||||
view,
|
||||
isDisabled,
|
||||
onViewSelect
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ViewMenu
|
||||
isDisabled={isDisabled}
|
||||
alignMenu={align.RIGHT}
|
||||
>
|
||||
<MenuContent>
|
||||
<ViewMenuItem
|
||||
name="table"
|
||||
selectedView={view}
|
||||
onPress={onViewSelect}
|
||||
>
|
||||
Table
|
||||
</ViewMenuItem>
|
||||
|
||||
<ViewMenuItem
|
||||
name="posters"
|
||||
selectedView={view}
|
||||
onPress={onViewSelect}
|
||||
>
|
||||
Posters
|
||||
</ViewMenuItem>
|
||||
|
||||
<ViewMenuItem
|
||||
name="overview"
|
||||
selectedView={view}
|
||||
onPress={onViewSelect}
|
||||
>
|
||||
Overview
|
||||
</ViewMenuItem>
|
||||
</MenuContent>
|
||||
</ViewMenu>
|
||||
);
|
||||
}
|
||||
|
||||
BookIndexViewMenu.propTypes = {
|
||||
view: PropTypes.string.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
onViewSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookIndexViewMenu;
|
||||
@@ -1,106 +0,0 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.container {
|
||||
&:hover {
|
||||
.content {
|
||||
background-color: $tableRowHoverBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.poster {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.editorSelect {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.posterContainer {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
display: block;
|
||||
color: $defaultColor;
|
||||
|
||||
&:hover {
|
||||
color: $defaultColor;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ended {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 0 25px 25px 0;
|
||||
border-style: solid;
|
||||
border-color: transparent $dangerColor transparent transparent;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex: 1 0 1px;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 10px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.title {
|
||||
@add-mixin truncate;
|
||||
composes: link;
|
||||
|
||||
flex: 1 0 1px;
|
||||
font-weight: 300;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
|
||||
.overview {
|
||||
composes: link;
|
||||
|
||||
flex: 0 1 1000px;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.overview {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import TextTruncate from 'react-text-truncate';
|
||||
import AuthorPoster from 'Author/AuthorPoster';
|
||||
import DeleteAuthorModal from 'Author/Delete/DeleteAuthorModal';
|
||||
import EditAuthorModalConnector from 'Author/Edit/EditAuthorModalConnector';
|
||||
import BookIndexProgressBar from 'Book/Index/ProgressBar/BookIndexProgressBar';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import stripHtml from 'Utilities/String/stripHtml';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import BookIndexOverviewInfo from './BookIndexOverviewInfo';
|
||||
import styles from './BookIndexOverview.css';
|
||||
|
||||
const columnPadding = parseInt(dimensions.authorIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.authorIndexColumnPaddingSmallScreen);
|
||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||
const lineHeight = parseFloat(fonts.lineHeight);
|
||||
|
||||
// Hardcoded height beased on line-height of 32 + bottom margin of 10.
|
||||
// Less side-effecty than using react-measure.
|
||||
const titleRowHeight = 42;
|
||||
|
||||
function getContentHeight(rowHeight, isSmallScreen) {
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
|
||||
return rowHeight - padding;
|
||||
}
|
||||
|
||||
class BookIndexOverview extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isEditAuthorModalOpen: 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
|
||||
|
||||
onEditAuthorPress = () => {
|
||||
this.setState({ isEditAuthorModalOpen: true });
|
||||
}
|
||||
|
||||
onEditAuthorModalClose = () => {
|
||||
this.setState({ isEditAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
onDeleteAuthorPress = () => {
|
||||
this.setState({
|
||||
isEditAuthorModalOpen: false,
|
||||
isDeleteAuthorModalOpen: true
|
||||
});
|
||||
}
|
||||
|
||||
onDeleteAuthorModalClose = () => {
|
||||
this.setState({ isDeleteAuthorModalOpen: false });
|
||||
}
|
||||
|
||||
onChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
title,
|
||||
monitored,
|
||||
titleSlug,
|
||||
nextAiring,
|
||||
statistics,
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
qualityProfile,
|
||||
overviewOptions,
|
||||
showSearchAction,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
rowHeight,
|
||||
isSmallScreen,
|
||||
isRefreshingBook,
|
||||
isSearchingBook,
|
||||
onRefreshBookPress,
|
||||
onSearchPress,
|
||||
isEditorActive,
|
||||
isSelected,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
sizeOnDisk,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
overview,
|
||||
isEditAuthorModalOpen,
|
||||
isDeleteAuthorModalOpen
|
||||
} = this.state;
|
||||
|
||||
const link = `/book/${titleSlug}`;
|
||||
|
||||
const elementStyle = {
|
||||
width: `${posterWidth}px`,
|
||||
height: `${posterHeight}px`,
|
||||
objectFit: 'contain'
|
||||
};
|
||||
|
||||
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
|
||||
const overviewHeight = contentHeight - titleRowHeight;
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.posterContainer}>
|
||||
{
|
||||
isEditorActive &&
|
||||
<div className={styles.editorSelect}>
|
||||
<CheckInput
|
||||
className={styles.checkInput}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
status === 'ended' &&
|
||||
<div
|
||||
className={styles.ended}
|
||||
title={translate('Ended')}
|
||||
/>
|
||||
}
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
style={elementStyle}
|
||||
to={link}
|
||||
>
|
||||
<AuthorPoster
|
||||
className={styles.poster}
|
||||
style={elementStyle}
|
||||
images={images}
|
||||
coverType={'cover'}
|
||||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
blurBackground={true}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<BookIndexProgressBar
|
||||
monitored={monitored}
|
||||
bookCount={bookCount}
|
||||
bookFileCount={bookFileCount}
|
||||
totalBookCount={totalBookCount}
|
||||
posterWidth={posterWidth}
|
||||
detailedProgressBar={overviewOptions.detailedProgressBar}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.info} style={{ maxHeight: contentHeight }}>
|
||||
<div className={styles.titleRow}>
|
||||
<Link
|
||||
className={styles.title}
|
||||
to={link}
|
||||
>
|
||||
{title}
|
||||
</Link>
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={icons.REFRESH}
|
||||
title={translate('RefreshBook')}
|
||||
isSpinning={isRefreshingBook}
|
||||
onPress={onRefreshBookPress}
|
||||
/>
|
||||
|
||||
{
|
||||
showSearchAction &&
|
||||
<SpinnerIconButton
|
||||
className={styles.action}
|
||||
name={icons.SEARCH}
|
||||
title={translate('SearchForMonitoredBooks')}
|
||||
isSpinning={isSearchingBook}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
}
|
||||
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
title={translate('EditAuthor')}
|
||||
onPress={this.onEditAuthorPress}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.details}>
|
||||
|
||||
<Link
|
||||
className={styles.overview}
|
||||
to={link}
|
||||
>
|
||||
<TextTruncate
|
||||
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
|
||||
text={stripHtml(overview)}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<BookIndexOverviewInfo
|
||||
height={overviewHeight}
|
||||
monitored={monitored}
|
||||
sizeOnDisk={sizeOnDisk}
|
||||
qualityProfile={qualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
longDateFormat={longDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
{...overviewOptions}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditAuthorModalConnector
|
||||
isOpen={isEditAuthorModalOpen}
|
||||
authorId={id}
|
||||
onModalClose={this.onEditAuthorModalClose}
|
||||
onDeleteAuthorPress={this.onDeleteAuthorPress}
|
||||
/>
|
||||
|
||||
<DeleteAuthorModal
|
||||
isOpen={isDeleteAuthorModalOpen}
|
||||
authorId={id}
|
||||
onModalClose={this.onDeleteAuthorModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookIndexOverview.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
posterHeight: PropTypes.number.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
overviewOptions: PropTypes.object.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
isRefreshingBook: PropTypes.bool.isRequired,
|
||||
isSearchingBook: PropTypes.bool.isRequired,
|
||||
onRefreshBookPress: PropTypes.func.isRequired,
|
||||
onSearchPress: PropTypes.func.isRequired,
|
||||
isEditorActive: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
BookIndexOverview.defaultProps = {
|
||||
statistics: {
|
||||
bookCount: 0,
|
||||
bookFileCount: 0,
|
||||
totalBookCount: 0
|
||||
}
|
||||
};
|
||||
|
||||
export default BookIndexOverview;
|
||||
@@ -1,12 +0,0 @@
|
||||
.infos {
|
||||
display: flex;
|
||||
flex: 0 0 250px;
|
||||
flex-direction: column;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.infos {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import BookIndexOverviewInfoRow from './BookIndexOverviewInfoRow';
|
||||
import styles from './BookIndexOverviewInfo.css';
|
||||
|
||||
const infoRowHeight = parseInt(dimensions.authorIndexOverviewInfoRowHeight);
|
||||
|
||||
const rows = [
|
||||
{
|
||||
name: 'monitored',
|
||||
showProp: 'showMonitored',
|
||||
valueProp: 'monitored'
|
||||
|
||||
},
|
||||
{
|
||||
name: 'qualityProfileId',
|
||||
showProp: 'showQualityProfile',
|
||||
valueProp: 'qualityProfile'
|
||||
},
|
||||
{
|
||||
name: 'releaseDate',
|
||||
showProp: 'showReleaseDate',
|
||||
valueProp: 'releaseDate'
|
||||
},
|
||||
{
|
||||
name: 'added',
|
||||
showProp: 'showAdded',
|
||||
valueProp: 'added'
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
showProp: 'showPath',
|
||||
valueProp: 'author'
|
||||
},
|
||||
{
|
||||
name: 'sizeOnDisk',
|
||||
showProp: 'showSizeOnDisk',
|
||||
valueProp: 'sizeOnDisk'
|
||||
}
|
||||
];
|
||||
|
||||
function isVisible(row, props) {
|
||||
const {
|
||||
name,
|
||||
showProp,
|
||||
valueProp
|
||||
} = row;
|
||||
|
||||
if (props[valueProp] == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return props[showProp] || props.sortKey === name;
|
||||
}
|
||||
|
||||
function getInfoRowProps(row, props) {
|
||||
const { name } = row;
|
||||
|
||||
if (name === 'monitored') {
|
||||
const monitoredText = props.monitored ? 'Monitored' : 'Unmonitored';
|
||||
|
||||
return {
|
||||
title: monitoredText,
|
||||
iconName: props.monitored ? icons.MONITORED : icons.UNMONITORED,
|
||||
label: monitoredText
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'qualityProfileId') {
|
||||
return {
|
||||
title: 'Quality Profile',
|
||||
iconName: icons.PROFILE,
|
||||
label: props.qualityProfile.name
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'releaseDate') {
|
||||
const {
|
||||
releaseDate,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
return {
|
||||
title: `ReleaseDate: ${formatDateTime(releaseDate, longDateFormat, timeFormat)}`,
|
||||
iconName: icons.CALENDAR,
|
||||
label: getRelativeDate(
|
||||
releaseDate,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
const {
|
||||
added,
|
||||
showRelativeDates,
|
||||
shortDateFormat,
|
||||
longDateFormat,
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
return {
|
||||
title: `Added: ${formatDateTime(added, longDateFormat, timeFormat)}`,
|
||||
iconName: icons.ADD,
|
||||
label: getRelativeDate(
|
||||
added,
|
||||
shortDateFormat,
|
||||
showRelativeDates,
|
||||
{
|
||||
timeFormat,
|
||||
timeForToday: true
|
||||
}
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return {
|
||||
title: 'Path',
|
||||
iconName: icons.FOLDER,
|
||||
label: props.author.path
|
||||
};
|
||||
}
|
||||
|
||||
if (name === 'sizeOnDisk') {
|
||||
return {
|
||||
title: 'Size on Disk',
|
||||
iconName: icons.DRIVE,
|
||||
label: formatBytes(props.sizeOnDisk)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function BookIndexOverviewInfo(props) {
|
||||
const {
|
||||
height
|
||||
} = props;
|
||||
|
||||
let shownRows = 1;
|
||||
|
||||
const maxRows = Math.floor(height / (infoRowHeight + 4));
|
||||
|
||||
return (
|
||||
<div className={styles.infos}>
|
||||
{
|
||||
rows.map((row) => {
|
||||
if (!isVisible(row, props)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (shownRows >= maxRows) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shownRows++;
|
||||
|
||||
const infoRowProps = getInfoRowProps(row, props);
|
||||
|
||||
return (
|
||||
<BookIndexOverviewInfoRow
|
||||
key={row.name}
|
||||
{...infoRowProps}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BookIndexOverviewInfo.propTypes = {
|
||||
height: PropTypes.number.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
showAdded: PropTypes.bool.isRequired,
|
||||
showReleaseDate: PropTypes.bool.isRequired,
|
||||
showPath: PropTypes.bool.isRequired,
|
||||
showSizeOnDisk: PropTypes.bool.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
author: PropTypes.object.isRequired,
|
||||
releaseDate: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
sizeOnDisk: PropTypes.number,
|
||||
sortKey: PropTypes.string.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default BookIndexOverviewInfo;
|
||||
@@ -1,10 +0,0 @@
|
||||
.infoRow {
|
||||
flex: 0 0 $authorIndexOverviewInfoRowHeight;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
width: 25px !important;
|
||||
text-align: center;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user