1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-03-06 13:31:28 -05:00

Compare commits

..

2 Commits

Author SHA1 Message Date
Qstick
2bca1a71a2 Bump version 3.2.2 2021-06-03 07:34:31 -04:00
Qstick
4f009bb81d Fixed: Use normal URL for Trakt Oauth per new docs 2021-06-03 07:33:27 -04:00
1622 changed files with 36618 additions and 47013 deletions

View File

@@ -19,10 +19,10 @@ indent_size = 4
dotnet_sort_system_directives_first = true
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
# Indentation preferences
csharp_indent_block_contents = true
@@ -32,6 +32,10 @@ csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _

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

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

View File

@@ -1,73 +0,0 @@
name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first'
labels: ['Type: Bug', 'Status: Needs Triage']
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the bug you encountered.
options:
- label: I have searched the existing issues
required: true
- type: textarea
attributes:
label: Current Behavior
description: A concise description of what you're experiencing.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
description: A concise description of what you expected to happen.
validations:
required: true
- type: textarea
attributes:
label: Steps To Reproduce
description: Steps to reproduce the behavior.
placeholder: |
1. In this environment...
2. With this config...
3. Run '...'
4. See error...
validations:
required: false
- type: textarea
attributes:
label: Environment
description: |
examples:
- **OS**: Ubuntu 20.04
- **Radarr**: Radarr 3.0.1.4259
- **Docker Install**: Yes
- **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related)
value: |
- OS:
- Radarr:
- Docker Install:
- Using Reverse Proxy:
- Browser:
render: markdown
validations:
required: true
- type: dropdown
attributes:
label: What branch are you running?
options:
- Master
- Develop
- Nightly
- Other (This issue will be closed)
validations:
required: true
- type: textarea
attributes:
label: Trace Logs?
description: |
Trace Logs (https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files)
***Generally speaking, all bug reports must have trace logs provided.***
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
Additionally, any additional info? Screenshots? References? Anything that will give us more context about the issue you are encountering!
validations:
required: true

View File

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

View File

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

View File

@@ -1,16 +1,15 @@
#### Database Migration
YES - XXXX | NO
YES | NO
#### Description
A few sentences describing the overall goals of the pull request's commits.
#### Screenshot (if UI related)
#### Todos
- [ ] Tests
- [ ] Translation Keys (./src/NzbDrone.Core/Localization/Core/en.json)
- [ ] [Wiki Updates](https://wiki.servarr.com)
- [ ] Translation Keys
- [ ] Wiki Updates
#### Issues Fixed or Closed by this PR
* Fixes #XXXX
* Fixes #XXXX

3
.github/stale.yml vendored
View File

@@ -4,8 +4,7 @@ daysUntilStale: 60
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- feature request #legacy
- 'Type: Feature Request'
- feature request
- 'Status: Confirmed'
- sonarr-pull
- lidarr-pull

View File

@@ -1,41 +0,0 @@
name: Sync issue to Azure DevOps work item
on:
issues:
types:
[opened, edited, deleted, closed, reopened, labeled, unlabeled, assigned]
concurrency: azuresync-${{ github.event.issue.number }}
jobs:
alert:
runs-on: ubuntu-latest
steps:
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == true }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "Bug"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100
- uses: danhellem/github-actions-issue-to-work-item@master
if: "${{ contains(github.event.issue.labels.*.name, 'Type: Bug') == false }}"
env:
ado_token: "${{ secrets.ADO_PERSONAL_ACCESS_TOKEN }}"
github_token: "${{ github.token }}"
ado_organization: "Servarr"
ado_project: "Servarr"
ado_area_path: "Servarr\\Radarr"
ado_wit: "User Story"
ado_new_state: "New"
ado_active_state: "Active"
ado_close_state: "Closed"
ado_bypassrules: true
log_level: 100

4
.gitignore vendored
View File

@@ -187,10 +187,6 @@ packages.config.md5sum
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
# ignore node_modules symlink
node_modules
node_modules.nosync
# API doc generation
.config/

View File

@@ -1,13 +1,49 @@
# How to Contribute
# How to Contribute #
We're always looking for people to help make Radarr 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/radarr/contributing).
## Documentation ##
Setup guides, FAQ, the more information we have on the [wiki](https://wiki.servarr.com/Radarr) the better.
## Documentation
## Development ##
Setup guides, [FAQ](https://wiki.servarr.com/radarr/faq), the more information we have on the [wiki](https://wiki.servarr.com/radarr) the better.
### Tools required ###
- Visual Studio 2019 or higher (https://www.visualstudio.com/vs/). The community version is free and works (https://www.visualstudio.com/downloads/).
- HTML/Javascript editor of choice (VS Code/Sublime Text/Webstorm/Atom/etc)
- [Git](https://git-scm.com/downloads)
- [NodeJS](https://nodejs.org/en/download/) (Node 12.X.X or higher)
- [Yarn](https://yarnpkg.com/)
- .NET Core 5.0.
## Development
### Getting started ###
See the [Wiki Page](https://wiki.servarr.com/radarr/contributing)
1. Fork Radarr
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 `Radarr.Console` and framework to `net5.0`
6. Debug the project in Visual Studio
7. Open http://localhost:7878
### Contributing Code ###
- If you're adding a new, already requested feature, please comment on [Github Issues](https://github.com/Radarr/Radarr/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 Radarr's develop branch, don't merge
- Make meaningful commits, or squash them
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
- Reach out to us on the discord if you have any questions
- Add tests (unit/integration)
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
- One feature/bug fix per pull request to keep things clean and easy to understand
- Use 4 spaces instead of tabs, this is the default for VS 2019 and WebStorm (to my knowledge)
### Pull Requesting ###
- Only make pull requests to develop, never master, if you make a PR to master we'll comment on it and close it
- You're probably going to get some comments or questions from us, they will be to ensure consistency and maintainability
- We'll try to respond to pull requests as soon as possible, if its been a day or two, please reach out to us, we may have missed it
- Each PR should come from its own [feature branch](http://martinfowler.com/bliki/FeatureBranch.html) not develop in your fork, it should have a meaningful branch name (what is being added/fixed)
- new-feature (Good)
- fix-bug (Good)
- patch (Bad)
- develop (Bad)
If you have any questions about any of this, please let us know.

View File

@@ -1,21 +1,19 @@
# Radarr
[![Build Status](https://dev.azure.com/Radarr/Radarr/_apis/build/status/Radarr.Radarr?branchName=develop)](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
[![Translated](https://translate.servarr.com/widgets/servarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/radarr/installation#docker)
[![Translated](https://translate.servarr.com/widgets/radarr/-/radarr/svg-badge.svg)](https://translate.servarr.com/engage/radarr/?utm_source=widget)
[![Docker Pulls](https://img.shields.io/docker/pulls/linuxserver/radarr.svg)](https://wiki.servarr.com/Radarr_Installation#Docker)
![Github Downloads](https://img.shields.io/github/downloads/Radarr/Radarr/total.svg)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Backers on Open Collective](https://opencollective.com/Radarr/backers/badge.svg)](#backers)
[![Sponsors on Open Collective](https://opencollective.com/Radarr/sponsors/badge.svg)](#sponsors)
[![Mega Sponsors on Open Collective](https://opencollective.com/Radarr/megasponsors/badge.svg)](#mega-sponsors)
Radarr is a movie collection manager for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new movies and will interface with clients and indexers to grab, sort, and rename them. It can also be configured to automatically upgrade the quality of existing files in the library when a better quality format becomes available.
Note that only one type of a given movie is supported. If you want both an 4k version and 1080p version of a given movie you will need multiple instances.
## Major Features Include
## Major Features Include:
* Adding new movies with lots of information, such as trailers, ratings, etc.
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
* Can watch for better quality of the movies you have and do an automatic upgrade. *e.g. from DVD to Blu-Ray*
* Can watch for better quality of the movies you have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
* 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
* Full integration with SABnzbd and NZBGet
@@ -23,60 +21,57 @@ Note that only one type of a given movie is supported. If you want both an 4k ve
* Automatically importing downloaded movies
* Recognizing Special Editions, Director's Cut, etc.
* Identifying releases with hardcoded subs
* Identifying releases with AKA movie names
* SABnzbd, NZBGet, QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported and integrated
* Full integration with Kodi and Plex (notifications, library updates)
* QBittorrent, Deluge, rTorrent, Transmission, uTorrent, and other download clients are supported
* Full integration with Kodi, Plex (notification, library update)
* A beautiful UI
* Importing Metadata such as trailers or subtitles
* Adding metadata such as posters and information for Kodi and others to use
* Advanced customization for profiles, such that Radarr will always download the copy you want
* A beautiful UI
## Support
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/radarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
Note: GitHub Issues are for Bugs and Feature Requests Only
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://radarr.video/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Radarr)
[![GitHub - Bugs and Feature Requests Only](https://img.shields.io/badge/github-issues-red.svg?maxAge=60)](https://github.com/Radarr/Radarr/issues)
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/Radarr)
## Feature Requests
[Feature Requests](https://github.com/Radarr/Radarr/issues/new?assignees=&labels=Type%3A+Enhancement&template=feature_request.md&title=)
## Contributors & Developers
[API Documentation](https://radarr.video/docs/api/)
This project exists thanks to all the people who contribute.
- [Contribute (GitHub)](CONTRIBUTING.md)
- [Contribution (Wiki Article)](https://wiki.servarr.com/radarr/contributing)
This project exists thanks to all the people who contribute. [Contribute](CONTRIBUTING.md).
<a href="https://github.com/Radarr/Radarr/graphs/contributors"><img src="https://opencollective.com/Radarr/contributors.svg?width=890&button=false" /></a>
[![Contributors List](https://opencollective.com/Radarr/contributors.svg?width=890&button=false)](https://github.com/Radarr/Radarr/graphs/contributors)
## Backers
Thank you to all our backers! 🙏 [Become a backer](https://opencollective.com/Radarr#backer)
[![Backers List](https://opencollective.com/Radarr/backers.svg?width=890)](https://opencollective.com/Radarr#backer)
<img src="https://opencollective.com/Radarr/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/Radarr#sponsor)
[![Sponsors List](https://opencollective.com/Radarr/sponsors.svg?width=890)](https://opencollective.com/Radarr#sponsor)
<img src="https://opencollective.com/Radarr/sponsors.svg?width=890"></a>
## Mega Sponsors
[![Mega Sponsors List](https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890)](https://opencollective.com/Radarr#mega-sponsor)
<img src="https://opencollective.com/Radarr/tiers/mega-sponsor.svg?width=890"></a>
## JetBrains
Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools.
* [<img src="/Logo/resharper.svg" alt="ReSharper" width="32"> ReSharper](http://www.jetbrains.com/resharper/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/)
* [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
* Copyright 2010-2022
* Copyright 2010-2021

View File

@@ -7,20 +7,14 @@ variables:
outputFolder: './_output'
artifactsFolder: './_artifacts'
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '4.2.0'
majorVersion: '3.2.2'
minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.300'
nodeVersion: '16.X'
innoVersion: '6.2.0'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
dotnetVersion: '5.0.202'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
trigger:
branches:
@@ -35,7 +29,6 @@ pr:
paths:
exclude:
- src/NzbDrone.Core/Localization/Core
- src/Radarr.Api.*/openapi.json
stages:
- stage: Setup
@@ -44,7 +37,7 @@ stages:
- job:
displayName: Build Variables
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: 'ubuntu-18.04'
steps:
# Set the build name properly. The 'name' property won't recursively expand so hack here:
- bash: echo "##vso[build.updatebuildnumber]$RADARRVERSION"
@@ -70,15 +63,15 @@ 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'
Windows:
osName: 'Windows'
imageName: ${{ variables.windowsImage }}
imageName: 'windows-2019'
enableAnalysis: 'false'
pool:
@@ -97,14 +90,15 @@ stages:
- bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled"
echo "BSD already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi
displayName: Enable Extra Platform Support
- bash: ./build.sh --backend --enable-extra-platforms
displayName: Enable FreeBSD Support
- bash: ./build.sh --backend --enable-bsd
displayName: Build Radarr Backend
- bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -117,29 +111,29 @@ stages:
artifact: '$(osName)Backend'
displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests
displayName: Publish win-x64 Test Package
- publish: '$(testsFolder)/net5.0/win-x64/publish'
artifact: WindowsCoreTests
displayName: Publish Windows Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests
displayName: Publish linux-x64 Test Package
- publish: '$(testsFolder)/net472/linux-x64/publish'
artifact: LinuxTests
displayName: Publish Linux Mono Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x86/publish'
artifact: linux-x86-tests
displayName: Publish linux-x86 Test Package
- publish: '$(testsFolder)/net5.0/linux-x64/publish'
artifact: LinuxCoreTests
displayName: Publish Linux Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests
displayName: Publish linux-musl-x64 Test Package
- publish: '$(testsFolder)/net5.0/linux-musl-x64/publish'
artifact: LinuxMuslCoreTests
displayName: Publish Linux Musl Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests
displayName: Publish freebsd-x64 Test Package
- publish: '$(testsFolder)/net5.0/freebsd-x64/publish'
artifact: FreebsdCoreTests
displayName: Publish FreeBSD Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests
displayName: Publish osx-x64 Test Package
- publish: '$(testsFolder)/net5.0/osx-x64/publish'
artifact: MacCoreTests
displayName: Publish MacOS Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend
@@ -151,20 +145,20 @@ 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:
- task: NodeTool@0
displayName: Set Node.js version
inputs:
versionSpec: $(nodeVersion)
versionSpec: '12.x'
- checkout: self
submodules: true
fetchDepth: 1
@@ -173,6 +167,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: |
yarn | "$(osName)"
yarn
path: $(yarnCacheFolder)
displayName: Cache Yarn packages
- bash: ./build.sh --frontend
@@ -193,7 +188,7 @@ stages:
- job: Windows_Installer
displayName: Create Installer
pool:
vmImage: ${{ variables.windowsImage }}
vmImage: 'windows-2019'
steps:
- checkout: self
fetchDepth: 1
@@ -209,11 +204,16 @@ stages:
artifactName: WindowsFrontend
targetPath: _output
displayName: Fetch Frontend
- bash: ./build.sh --packages
displayName: Create Packages
- bash: |
./build.sh --packages --installer
cp setup/output/Radarr.*win-x64.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
cp setup/output/Radarr.*win-x86.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create Installers
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x86
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x86-installer.exe
displayName: Create .NET Core Windows installer
- bash: |
setup/inno/ISCC.exe setup/radarr.iss //DFramework=net5.0 //DRuntime=win-x64
cp setup/output/Radarr.*windows.net5.0.exe ${BUILD_ARTIFACTSTAGINGDIRECTORY}/Radarr.${BUILDNAME}.windows-core-x64-installer.exe
displayName: Create .NET Core Windows installer
- publish: $(Build.ArtifactStagingDirectory)
artifact: 'WindowsInstaller'
displayName: Publish Installer
@@ -226,7 +226,7 @@ stages:
- job: Other_Packages
displayName: Create Standard Packages
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: 'ubuntu-18.04'
steps:
- checkout: self
fetchDepth: 1
@@ -242,121 +242,97 @@ 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 "ffprobe" -exec chmod a+x {} \;
find . -name "Radarr" -exec chmod a+x {} \;
find . -name "Radarr.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)/Radarr.$(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)/Radarr.$(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)/Radarr.$(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)/Radarr.$(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
displayName: Create Linux Mono tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(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)/Radarr.$(buildName).osx-core-arm64.tar.gz'
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux.tar.gz'
archiveType: 'tar'
tarCompression: 'gz'
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
rootFolderOrFile: $(artifactsFolder)/linux-x64/net472
- task: ArchiveFiles@2
displayName: Create linux-x64 tar
displayName: Create Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(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)/Radarr.$(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)/Radarr.$(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)/Radarr.$(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)/Radarr.$(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 ARM64 Linux Core tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(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)/Radarr.$(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)/Radarr.$(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
@@ -395,7 +371,7 @@ stages:
jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: 'ubuntu-18.04'
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
@@ -417,22 +393,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:
@@ -451,15 +427,26 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)-tests'
artifactName: '$(testName)Tests'
targetPath: $(testsFolder)
- bash: |
wget https://mediaarea.net/repo/deb/repo-mediaarea_1.0-11_all.deb
sudo dpkg -i repo-mediaarea_1.0-11_all.deb
sudo apt-get update
sudo apt-get install -y --allow-unauthenticated libmediainfo-dev libmediainfo0v5 mediainfo
displayName: Install mediainfo
condition: and(succeeded(), eq(variables['testName'], 'LinuxCore'))
- powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- bash: |
chmod a+x _tests/ffprobe
displayName: Make ffprobe Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
SYMLINK=6_6_0
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
echo "##vso[task.setvariable variable=MONOPREFIX;]$MONOPREFIX"
echo "##vso[task.setvariable variable=PKG_CONFIG_PATH;]$MONOPREFIX/lib/pkgconfig:$MONOPREFIX/share/pkgconfig:$PKG_CONFIG_PATH"
echo "##vso[task.setvariable variable=PATH;]$MONOPREFIX/bin:$PATH"
displayName: Set Mono Version
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
@@ -483,73 +470,27 @@ stages:
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy:
matrix:
mono520:
testName: 'Mono 5.20'
artifactName: LinuxTests
containerImage: ghcr.io/servarr/testimages:mono-5.20
mono610:
testName: 'Mono 6.10'
artifactName: LinuxTests
containerImage: ghcr.io/servarr/testimages:mono-6.10
mono612:
testName: 'Mono 6.12'
artifactName: LinuxTests
containerImage: ghcr.io/servarr/testimages:mono-6.12
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 }}
container: $[ variables['containerImage'] ]
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .NET'
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
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: |
chmod a+x _tests/ffprobe
displayName: Make ffprobe Executable
- bash: find ${TESTSFOLDER} -name "Radarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- 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: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Radarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Radarr__Postgres__Host: 'localhost'
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: 'ubuntu-18.04'
container: $[ variables['containerImage'] ]
timeoutInMinutes: 10
@@ -565,19 +506,9 @@ stages:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: |
chmod a+x _tests/ffprobe
displayName: Make ffprobe Executable
- bash: find ${TESTSFOLDER} -name "Radarr.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=radarr \
-e POSTGRES_USER=radarr \
-p 5432:5432/tcp \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
@@ -588,7 +519,7 @@ stages:
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- stage: Integration
@@ -598,7 +529,7 @@ stages:
jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: 'ubuntu-18.04'
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
@@ -617,24 +548,32 @@ stages:
matrix:
MacCore:
osName: 'Mac'
testName: 'osx-x64'
imageName: ${{ variables.macImage }}
testName: 'MacCore'
imageName: 'macos-10.14'
pattern: 'Radarr.*.osx-core-x64.tar.gz'
WindowsCore:
osName: 'Windows'
testName: 'win-x64'
imageName: ${{ variables.windowsImage }}
testName: 'WindowsCore'
imageName: 'windows-2019'
pattern: 'Radarr.*.windows-core-x64.zip'
LinuxCore:
osName: 'Linux'
testName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
testName: 'LinuxCore'
imageName: 'ubuntu-18.04'
pattern: 'Radarr.*.linux-core-x64.tar.gz'
pool:
vmImage: $(imageName)
steps:
- bash: |
SYMLINK=6_6_0
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
echo "##vso[task.setvariable variable=MONOPREFIX;]$MONOPREFIX"
echo "##vso[task.setvariable variable=PKG_CONFIG_PATH;]$MONOPREFIX/lib/pkgconfig:$MONOPREFIX/share/pkgconfig:$PKG_CONFIG_PATH"
echo "##vso[task.setvariable variable=PATH;]$MONOPREFIX/bin:$PATH"
displayName: Set Mono Version
condition: and(succeeded(), eq(variables['osName'], 'Mac'))
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
@@ -644,7 +583,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(testName)-tests'
artifactName: '$(testName)Tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -674,67 +613,6 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Radarr.*.linux-core-x64.tar.gz'
Radarr__Postgres__Host: 'localhost'
Radarr__Postgres__Port: '5432'
Radarr__Postgres__User: 'radarr'
Radarr__Postgres__Password: 'radarr'
pool:
vmImage: 'ubuntu-18.04'
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/Radarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \
-p 5432:5432/tcp \
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
dependsOn: Prepare
@@ -752,7 +630,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'freebsd-x64-tests'
artifactName: 'FreebsdCoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -787,18 +665,28 @@ stages:
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
strategy:
matrix:
mono520:
testName: 'Mono 5.20'
artifactName: LinuxTests
containerImage: ghcr.io/servarr/testimages:mono-5.20
pattern: 'Radarr.*.linux.tar.gz'
mono610:
testName: 'Mono 6.10'
artifactName: LinuxTests
containerImage: ghcr.io/servarr/testimages:mono-6.10
pattern: 'Radarr.*.linux.tar.gz'
mono612:
testName: 'Mono 6.12'
artifactName: LinuxTests
containerImage: ghcr.io/servarr/testimages:mono-6.12
pattern: 'Radarr.*.linux.tar.gz'
alpine:
testName: 'linux-musl-x64'
artifactName: linux-musl-x64-tests
testName: 'Musl Net Core'
artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz'
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pattern: 'Radarr.*.linux-core-x86.tar.gz'
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: 'ubuntu-18.04'
container: $[ variables['containerImage'] ]
@@ -806,15 +694,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
@@ -860,20 +742,17 @@ stages:
matrix:
Linux:
osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }}
imageName: 'ubuntu-18.04'
pattern: 'Radarr.*.linux-core-x64.tar.gz'
failBuild: true
Mac:
osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }}
imageName: 'macos-10.14'
pattern: 'Radarr.*.osx-core-x64.tar.gz'
failBuild: true
Windows:
osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }}
imageName: 'windows-2019'
pattern: 'Radarr.*.windows-core-x64.zip'
failBuild: true
@@ -890,7 +769,7 @@ stages:
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: '$(artifactName)-tests'
artifactName: '$(osName)CoreTests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
@@ -939,7 +818,7 @@ stages:
jobs:
- job: Prepare
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: 'ubuntu-18.04'
steps:
- checkout: none
- task: DownloadPipelineArtifact@2
@@ -956,17 +835,17 @@ 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:
- task: NodeTool@0
displayName: Set Node.js version
inputs:
versionSpec: $(nodeVersion)
versionSpec: '12.x'
- checkout: self
submodules: true
fetchDepth: 1
@@ -975,6 +854,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: |
yarn | "$(osName)"
yarn
path: $(yarnCacheFolder)
displayName: Cache Yarn packages
- bash: ./build.sh --lint
@@ -987,7 +867,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
@@ -1003,60 +883,6 @@ stages:
cliProjectVersion: '$(radarrVersion)'
cliSources: './frontend'
- task: SonarCloudAnalyze@1
- job: Api_Docs
displayName: API Docs
dependsOn: Prepare
condition: |
and
(
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
)
pool:
vmImage: ${{ variables.windowsImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: self
submodules: true
persistCredentials: true
fetchDepth: 1
- bash: ./docs.sh Windows
displayName: Create openapi.json
- bash: |
git config --global user.email "development@lidarr.audio"
git config --global user.name "Servarr"
git checkout -b api-docs
git add .
git status
if git status | grep modified
then
git commit -am 'Automated API Docs update'
git push -f --set-upstream origin api-docs
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/radarr/radarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else
echo "No changes since last run"
fi
displayName: Commit API Doc Change
continueOnError: true
env:
GITHUBTOKEN: $(githubToken)
- task: CopyFiles@2
displayName: 'Copy openapi.json to: $(Build.ArtifactStagingDirectory)'
inputs:
SourceFolder: '$(Build.SourcesDirectory)'
Contents: |
**/*openapi.json
TargetFolder: '$(Build.ArtifactStagingDirectory)/api_docs'
- publish: $(Build.ArtifactStagingDirectory)/api_docs
artifact: 'APIDocs'
displayName: Publish API Docs Bundle
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
- job: Analyze_Backend
displayName: Backend
@@ -1068,7 +894,7 @@ stages:
EnableAnalyzers: 'false'
pool:
vmImage: ${{ variables.windowsImage }}
vmImage: windows-2019
steps:
- task: UseDotNet@2
@@ -1090,12 +916,12 @@ stages:
projectVersion: '$(radarrVersion)'
extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
sonar.coverage.exclusions=**/Radarr.Api.V3/**/*,**/NzbDrone.Api/**/*,**/MonoTorrent/**/*,**/Marr.Data/**/*
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 win-x64
TEST_DIR=_tests/net6.0/win-x64/publish/ ./test.sh Windows Unit Coverage
./build.sh --backend -f net5.0 -r win-x64
TEST_DIR=_tests/net5.0/win-x64/publish/ ./test.sh Windows Unit Coverage
displayName: Coverage Unit Tests
- task: SonarCloudAnalyze@1
condition: eq(variables['System.PullRequest.IsFork'], 'False')
@@ -1125,7 +951,7 @@ stages:
- job:
displayName: Discord Notification
pool:
vmImage: ${{ variables.linuxImage }}
vmImage: 'windows-2019'
steps:
- task: DownloadPipelineArtifact@2
continueOnError: true
@@ -1135,7 +961,7 @@ stages:
artifactName: 'WindowsAutomationScreenshots'
targetPath: $(Build.SourcesDirectory)
- checkout: none
- pwsh: |
- powershell: |
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)

145
build.sh
View File

@@ -25,22 +25,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
}
@@ -137,7 +130,7 @@ PackageLinux()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net5.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -148,13 +141,17 @@ PackageLinux()
PackageMacOS()
{
local framework="$1"
local runtime="$2"
ProgressStart "Creating MacOS Package for $framework $runtime"
ProgressStart "Creating MacOS Package for $framework"
local folder=$artifactsFolder/$runtime/$framework/Radarr
local folder=$artifactsFolder/macos/$framework/Radarr
PackageFiles "$folder" "$framework" "$runtime"
PackageFiles "$folder" "$framework" "osx-x64"
if [ "$framework" = "net472" ]; then
echo "Adding Startup script"
cp macOS/Radarr $folder
fi
echo "Removing Service helpers"
rm -f $folder/ServiceUninstall.*
@@ -165,7 +162,7 @@ PackageMacOS()
echo "Adding Radarr.Mono to UpdatePackage"
cp $folder/Radarr.Mono.* $folder/Radarr.Update
if [ "$framework" = "net6.0" ]; then
if [ "$framework" = "net5.0" ]; then
cp $folder/Mono.Posix.NETStandard.* $folder/Radarr.Update
cp $folder/libMonoPosixHelper.* $folder/Radarr.Update
fi
@@ -176,11 +173,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
@@ -188,7 +184,7 @@ PackageMacOSApp()
mkdir -p $folder/Radarr.app/Contents/MacOS
echo "Copying Binaries"
cp -r $artifactsFolder/$runtime/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
cp -r $artifactsFolder/macos/$framework/Radarr/* $folder/Radarr.app/Contents/MacOS
echo "Removing Update Folder"
rm -r $folder/Radarr.app/Contents/MacOS/Radarr.Update
@@ -235,38 +231,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/radarr.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"
@@ -298,10 +268,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 ]]
@@ -313,12 +281,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)
@@ -339,10 +303,6 @@ case $key in
PACKAGES=YES
shift # past argument
;;
--installer)
INSTALLER=YES
shift # past argument
;;
--lint)
LINT=YES
shift # past argument
@@ -362,30 +322,25 @@ 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"
PackageTests "net472" "linux-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"
@@ -414,30 +369,20 @@ 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"
Package "net472" "linux-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

38
docs.sh
View File

@@ -1,38 +0,0 @@
PLATFORM=$1
if [ "$PLATFORM" = "Windows" ]; then
RUNTIME="win-x64"
elif [ "$PLATFORM" = "Linux" ]; then
WHERE="linux-x64"
elif [ "$PLATFORM" = "Mac" ]; then
WHERE="osx-x64"
else
echo "Platform must be provided as first arguement: Windows, Linux or Mac"
exit 1
fi
outputFolder='_output'
testPackageFolder='_tests'
rm -rf $outputFolder
rm -rf $testPackageFolder
slnFile=src/Radarr.sln
platform=Posix
dotnet clean $slnFile -c Debug
dotnet clean $slnFile -c Release
dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p:RuntimeIdentifiers=$RUNTIME -t:PublishAllRids
dotnet new tool-manifest
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
sleep 45
kill %1
exit 0

View File

@@ -1,6 +1,6 @@
const path = require('path');
const webpack = require('webpack');
const FileManagerPlugin = require('filemanager-webpack-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
@@ -23,7 +23,7 @@ module.exports = (env) => {
const config = {
mode: isProduction ? 'production' : 'development',
devtool: isProduction ? 'source-map' : 'eval-source-map',
devtool: 'source-map',
stats: {
children: false
@@ -87,47 +87,46 @@ module.exports = (env) => {
}),
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
template: 'frontend/src/index.html',
filename: 'index.html',
publicPath: '/'
}),
new FileManagerPlugin({
events: {
onEnd: {
copy: [
// HTML
{
source: 'frontend/src/*.html',
destination: distFolder
},
new CopyPlugin({
patterns: [
// HTML
{
from: 'frontend/src/*.html',
to: path.join(distFolder, '[name][ext]'),
globOptions: {
ignore: ['**/index.html']
}
},
// Fonts
{
source: 'frontend/src/Content/Fonts/*.*',
destination: path.join(distFolder, 'Content/Fonts')
},
// Fonts
{
from: 'frontend/src/Content/Fonts/*.*',
to: path.join(distFolder, 'Content/Fonts', '[name][ext]')
},
// Icon Images
{
source: 'frontend/src/Content/Images/Icons/*.*',
destination: path.join(distFolder, 'Content/Images/Icons')
},
// Icon Images
{
from: 'frontend/src/Content/Images/Icons/*.*',
to: path.join(distFolder, 'Content/Images/Icons', '[name][ext]')
},
// Images
{
source: 'frontend/src/Content/Images/*.*',
destination: path.join(distFolder, 'Content/Images')
},
// Images
{
from: 'frontend/src/Content/Images/*.*',
to: path.join(distFolder, 'Content/Images', '[name][ext]')
},
// Robots
{
source: 'frontend/src/Content/robots.txt',
destination: path.join(distFolder, 'Content/robots.txt')
}
]
// Robots
{
from: 'frontend/src/Content/robots.txt',
to: path.join(distFolder, 'Content', '[name][ext]')
}
}
]
}),
new LiveReloadPlugin()
@@ -223,7 +222,7 @@ module.exports = (env) => {
{
loader: 'url-loader',
options: {
limit: 24096,
limit: 10240,
mimetype: 'application/font-woff',
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
@@ -233,12 +232,11 @@ module.exports = (env) => {
},
{
test: /\.(ttf|eot|eot?#iefix|gif|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
test: /\.(ttf|eot|eot?#iefix|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
limit: 24096,
emitFile: false,
name: 'Content/Fonts/[name].[ext]'
}

View File

@@ -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
@@ -61,33 +61,33 @@ class Blocklist extends Component {
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
}
//
// Listeners
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true });
};
}
onRemoveSelectedConfirmed = () => {
this.props.onRemoveSelected(this.getSelectedIds());
this.setState({ isConfirmRemoveModalOpen: false });
};
}
onConfirmRemoveModalClose = () => {
this.setState({ isConfirmRemoveModalOpen: false });
};
}
//
// Render
@@ -101,8 +101,8 @@ class Blocklist extends Component {
columns,
totalRecords,
isRemoving,
isClearingBlocklistExecuting,
onClearBlocklistPress,
isClearingBlacklistExecuting,
onClearBlacklistPress,
...otherProps
} = this.props;
@@ -116,11 +116,11 @@ class Blocklist extends Component {
const selectedIds = this.getSelectedIds();
return (
<PageContent title={translate('Blocklist')}>
<PageContent title={translate('Blacklist')}>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RemoveSelected')}
label="Remove Selected"
iconName={icons.REMOVE}
isDisabled={!selectedIds.length}
isSpinning={isRemoving}
@@ -130,8 +130,8 @@ class Blocklist extends Component {
<PageToolbarButton
label={translate('Clear')}
iconName={icons.CLEAR}
isSpinning={isClearingBlocklistExecuting}
onPress={onClearBlocklistPress}
isSpinning={isClearingBlacklistExecuting}
onPress={onClearBlacklistPress}
/>
</PageToolbarSection>
@@ -157,7 +157,7 @@ class Blocklist extends Component {
{
!isFetching && !!error &&
<div>
{translate('UnableToLoadBlocklist')}
{translate('UnableToLoadBlacklist')}
</div>
}
@@ -181,13 +181,12 @@ class Blocklist extends Component {
>
<TableBody>
{
items.map((item, index) => {
items.map((item) => {
return (
<BlocklistRowConnector
<BlacklistRowConnector
key={item.id}
isSelected={selectedState[item.id] || false}
columns={columns}
index={index}
{...item}
onSelectedChange={this.onSelectedChange}
/>
@@ -210,7 +209,7 @@ class Blocklist extends Component {
isOpen={isConfirmRemoveModalOpen}
kind={kinds.DANGER}
title={translate('RemoveSelected')}
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlocklist')}
message={translate('AreYouSureYouWantToRemoveTheSelectedItemsFromBlacklist')}
confirmLabel={translate('RemoveSelected')}
onConfirm={this.onRemoveSelectedConfirmed}
onCancel={this.onConfirmRemoveModalClose}
@@ -220,7 +219,7 @@ class Blocklist extends Component {
}
}
Blocklist.propTypes = {
Blacklist.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
error: PropTypes.object,
@@ -228,9 +227,9 @@ Blocklist.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isRemoving: PropTypes.bool.isRequired,
isClearingBlocklistExecuting: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
onRemoveSelected: PropTypes.func.isRequired,
onClearBlocklistPress: PropTypes.func.isRequired
onClearBlacklistPress: PropTypes.func.isRequired
};
export default Blocklist;
export default Blacklist;

View File

@@ -0,0 +1,160 @@
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 withCurrentPage from 'Components/withCurrentPage';
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 Blacklist from './Blacklist';
function createMapStateToProps() {
return createSelector(
(state) => state.blacklist,
createCommandExecutingSelector(commandNames.CLEAR_BLACKLIST),
(blacklist, isClearingBlacklistExecuting) => {
return {
isClearingBlacklistExecuting,
...blacklist
};
}
);
}
const mapDispatchToProps = {
...blacklistActions,
executeCommand
};
class BlacklistConnector extends Component {
//
// Lifecycle
componentDidMount() {
const {
useCurrentPage,
fetchBlacklist,
gotoBlacklistFirstPage
} = this.props;
registerPagePopulator(this.repopulate);
if (useCurrentPage) {
fetchBlacklist();
} else {
gotoBlacklistFirstPage();
}
}
componentDidUpdate(prevProps) {
if (prevProps.isClearingBlacklistExecuting && !this.props.isClearingBlacklistExecuting) {
this.props.gotoBlacklistFirstPage();
}
}
componentWillUnmount() {
this.props.clearBlacklist();
unregisterPagePopulator(this.repopulate);
}
//
// Control
repopulate = () => {
this.props.fetchBlacklist();
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoBlacklistFirstPage();
}
onPreviousPagePress = () => {
this.props.gotoBlacklistPreviousPage();
}
onNextPagePress = () => {
this.props.gotoBlacklistNextPage();
}
onLastPagePress = () => {
this.props.gotoBlacklistLastPage();
}
onPageSelect = (page) => {
this.props.gotoBlacklistPage({ page });
}
onRemoveSelected = (ids) => {
this.props.removeBlacklistItems({ ids });
}
onSortPress = (sortKey) => {
this.props.setBlacklistSort({ sortKey });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
onClearBlacklistPress = () => {
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
}
onTableOptionChange = (payload) => {
this.props.setBlacklistTableOption(payload);
if (payload.pageSize) {
this.props.gotoBlacklistFirstPage();
}
}
//
// Render
render() {
return (
<Blacklist
onFirstPagePress={this.onFirstPagePress}
onPreviousPagePress={this.onPreviousPagePress}
onNextPagePress={this.onNextPagePress}
onLastPagePress={this.onLastPagePress}
onPageSelect={this.onPageSelect}
onRemoveSelected={this.onRemoveSelected}
onSortPress={this.onSortPress}
onTableOptionChange={this.onTableOptionChange}
onClearBlacklistPress={this.onClearBlacklistPress}
{...this.props}
/>
);
}
}
BlacklistConnector.propTypes = {
useCurrentPage: PropTypes.bool.isRequired,
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
fetchBlacklist: PropTypes.func.isRequired,
gotoBlacklistFirstPage: PropTypes.func.isRequired,
gotoBlacklistPreviousPage: PropTypes.func.isRequired,
gotoBlacklistNextPage: PropTypes.func.isRequired,
gotoBlacklistLastPage: PropTypes.func.isRequired,
gotoBlacklistPage: PropTypes.func.isRequired,
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)(BlacklistConnector)
);

View File

@@ -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;

View File

@@ -11,10 +11,10 @@ import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink';
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
@@ -32,11 +32,11 @@ class BlocklistRow extends Component {
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
}
//
// Render
@@ -166,7 +166,7 @@ class BlocklistRow extends Component {
/>
<IconButton
title={translate('RemoveFromBlocklist')}
title={translate('RemoveFromBlacklist')}
name={icons.REMOVE}
kind={kinds.DANGER}
onPress={onRemovePress}
@@ -179,7 +179,7 @@ class BlocklistRow extends Component {
})
}
<BlocklistDetailsModal
<BlacklistDetailsModal
isOpen={this.state.isDetailsModalOpen}
sourceTitle={sourceTitle}
protocol={protocol}
@@ -193,7 +193,7 @@ class BlocklistRow extends Component {
}
BlocklistRow.propTypes = {
BlacklistRow.propTypes = {
id: PropTypes.number.isRequired,
movie: PropTypes.object.isRequired,
sourceTitle: PropTypes.string.isRequired,
@@ -210,4 +210,4 @@ BlocklistRow.propTypes = {
onRemovePress: PropTypes.func.isRequired
};
export default BlocklistRow;
export default BlacklistRow;

View File

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

View File

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

View File

@@ -25,7 +25,6 @@ function HistoryDetails(props) {
releaseGroup,
nzbInfoUrl,
downloadClient,
downloadClientName,
downloadId,
age,
ageHours,
@@ -33,8 +32,6 @@ function HistoryDetails(props) {
publishedDate
} = data;
const downloadClientNameInfo = downloadClientName ?? downloadClient;
return (
<DescriptionList>
<DescriptionListItem
@@ -74,12 +71,11 @@ function HistoryDetails(props) {
}
{
downloadClientNameInfo ?
!!downloadClient &&
<DescriptionListItem
title={translate('DownloadClient')}
data={downloadClientNameInfo}
/> :
null
data={downloadClient}
/>
}
{

View File

@@ -57,38 +57,38 @@ class HistoryConnector extends Component {
repopulate = () => {
this.props.fetchHistory();
};
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoHistoryFirstPage();
};
}
onPreviousPagePress = () => {
this.props.gotoHistoryPreviousPage();
};
}
onNextPagePress = () => {
this.props.gotoHistoryNextPage();
};
}
onLastPagePress = () => {
this.props.gotoHistoryLastPage();
};
}
onPageSelect = (page) => {
this.props.gotoHistoryPage({ page });
};
}
onSortPress = (sortKey) => {
this.props.setHistorySort({ sortKey });
};
}
onFilterSelect = (selectedFilterKey) => {
this.props.setHistoryFilter({ selectedFilterKey });
};
}
onTableOptionChange = (payload) => {
this.props.setHistoryTableOption(payload);
@@ -96,7 +96,7 @@ class HistoryConnector extends Component {
if (payload.pageSize) {
this.props.gotoHistoryFirstPage();
}
};
}
//
// Render

View File

@@ -41,11 +41,11 @@ class HistoryRow extends Component {
onDetailsPress = () => {
this.setState({ isDetailsModalOpen: true });
};
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
}
//
// Render
@@ -179,16 +179,6 @@ class HistoryRow extends Component {
);
}
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') {
return (
<TableRowCell

View File

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

View File

@@ -90,45 +90,45 @@ class Queue extends Component {
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState);
};
}
//
// Listeners
onQueueRowModalOpenOrClose = (isOpen) => {
this._shouldBlockRefresh = isOpen;
};
}
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
}
onGrabSelectedPress = () => {
this.props.onGrabSelectedPress(this.getSelectedIds());
};
}
onRemoveSelectedPress = () => {
this.setState({ isConfirmRemoveModalOpen: true }, () => {
this._shouldBlockRefresh = true;
});
};
}
onRemoveSelectedConfirmed = (payload) => {
this._shouldBlockRefresh = false;
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
this.setState({ isConfirmRemoveModalOpen: false });
};
}
onConfirmRemoveModalClose = () => {
this._shouldBlockRefresh = false;
this.setState({ isConfirmRemoveModalOpen: false });
};
}
//
// Render
@@ -282,17 +282,6 @@ class Queue extends Component {
return !!(item && item.movieId);
})
)}
allPending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
if (!item) {
return false;
}
return item.status === 'delay' || item.status === 'downloadClientUnavailable';
})
)}
onRemovePress={this.onRemoveSelectedConfirmed}
onModalClose={this.onConfirmRemoveModalClose}
/>

View File

@@ -77,34 +77,34 @@ class QueueConnector extends Component {
repopulate = () => {
this.props.fetchQueue();
};
}
//
// Listeners
onFirstPagePress = () => {
this.props.gotoQueueFirstPage();
};
}
onPreviousPagePress = () => {
this.props.gotoQueuePreviousPage();
};
}
onNextPagePress = () => {
this.props.gotoQueueNextPage();
};
}
onLastPagePress = () => {
this.props.gotoQueueLastPage();
};
}
onPageSelect = (page) => {
this.props.gotoQueuePage({ page });
};
}
onSortPress = (sortKey) => {
this.props.setQueueSort({ sortKey });
};
}
onTableOptionChange = (payload) => {
this.props.setQueueTableOption(payload);
@@ -112,21 +112,21 @@ class QueueConnector extends Component {
if (payload.pageSize) {
this.props.gotoQueueFirstPage();
}
};
}
onRefreshPress = () => {
this.props.executeCommand({
name: commandNames.REFRESH_MONITORED_DOWNLOADS
});
};
}
onGrabSelectedPress = (ids) => {
this.props.grabQueueItems({ ids });
};
}
onRemoveSelectedPress = (payload) => {
this.props.removeQueueItems(payload);
};
}
//
// Render

View File

@@ -42,7 +42,7 @@ class QueueOptions extends Component {
[name]: value
});
});
};
}
//
// Render

View File

@@ -40,37 +40,37 @@ class QueueRow extends Component {
onRemoveQueueItemPress = () => {
this.setState({ isRemoveQueueItemModalOpen: true });
};
}
onRemoveQueueItemModalConfirmed = (blocklist) => {
onRemoveQueueItemModalConfirmed = (blacklist) => {
const {
onRemoveQueueItemPress,
onQueueRowModalOpenOrClose
} = this.props;
onQueueRowModalOpenOrClose(false);
onRemoveQueueItemPress(blocklist);
onRemoveQueueItemPress(blacklist);
this.setState({ isRemoveQueueItemModalOpen: false });
};
}
onRemoveQueueItemModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isRemoveQueueItemModalOpen: false });
};
}
onInteractiveImportPress = () => {
this.props.onQueueRowModalOpenOrClose(true);
this.setState({ isInteractiveImportModalOpen: true });
};
}
onInteractiveImportModalClose = () => {
this.props.onQueueRowModalOpenOrClose(false);
this.setState({ isInteractiveImportModalOpen: false });
};
}
//
// Render
@@ -332,7 +332,6 @@ class QueueRow extends Component {
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canIgnore={!!movie}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
onModalClose={this.onRemoveQueueItemModalClose}
/>

View File

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

View File

@@ -22,7 +22,7 @@ class RemoveQueueItemModal extends Component {
this.state = {
remove: true,
blocklist: false
blacklist: false
};
}
@@ -32,32 +32,32 @@ class RemoveQueueItemModal extends Component {
resetState = function() {
this.setState({
remove: true,
blocklist: false
blacklist: false
});
};
}
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
}
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
}
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
}
//
// Render
@@ -66,11 +66,10 @@ class RemoveQueueItemModal extends Component {
const {
isOpen,
sourceTitle,
canIgnore,
isPending
canIgnore
} = this.props;
const { remove, blocklist } = this.state;
const { remove, blacklist } = this.state;
return (
<Modal
@@ -90,31 +89,27 @@ class RemoveQueueItemModal extends Component {
{translate('RemoveFromQueueText', [sourceTitle])}
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
<FormLabel>{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>
@@ -142,7 +137,6 @@ RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -23,42 +23,42 @@ class RemoveQueueItemsModal extends Component {
this.state = {
remove: true,
blocklist: false
blacklist: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false
});
};
resetState = function() {
this.setState({
remove: true,
blacklist: false
});
}
//
// Listeners
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
}
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onBlacklistChange = ({ value }) => {
this.setState({ blacklist: value });
}
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
}
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
}
//
// Render
@@ -67,11 +67,10 @@ class RemoveQueueItemsModal extends Component {
const {
isOpen,
selectedCount,
canIgnore,
allPending
canIgnore
} = this.props;
const { remove, blocklist } = this.state;
const { remove, blacklist } = this.state;
return (
<Modal
@@ -83,42 +82,38 @@ class RemoveQueueItemsModal extends Component {
onModalClose={this.onModalClose}
>
<ModalHeader>
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
Remove Selected Item{selectedCount > 1 ? 's' : ''}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
{selectedCount > 1 ? translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', selectedCount) : translate('AreYouSureYouWantToRemoveSelectedItemFromQueue')}
{translate('AreYouSureYouWantToRemoveSelectedItemsFromQueue', [selectedCount, selectedCount > 1 ? 's' : ''])}
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
Blacklist Release{selectedCount > 1 ? 's' : ''}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistHelpText')}
onChange={this.onBlocklistChange}
name="blacklist"
value={blacklist}
helpText={translate('BlacklistHelpText')}
onChange={this.onBlacklistChange}
/>
</FormGroup>
@@ -146,7 +141,6 @@ RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -67,12 +67,12 @@ class AddNewMovie extends Component {
this.props.onClearMovieLookup();
}
});
};
}
onClearMovieLookupPress = () => {
this.setState({ term: '' });
this.props.onClearMovieLookup();
};
}
//
// Render
@@ -103,7 +103,7 @@ class AddNewMovie extends Component {
className={styles.searchInput}
name="movieLookup"
value={term}
placeholder="e.g. The Dark Knight, tmdb:155, imdb:tt0468569"
placeholder="eg. The Dark Knight, tmdb:155, imdb:tt0468569"
autoFocus={true}
onChange={this.onSearchInputChange}
/>
@@ -161,7 +161,7 @@ class AddNewMovie extends Component {
{translate('YouCanAlsoSearch')}
</div>
<div>
<Link to="https://wiki.servarr.com/radarr/faq#why-can-i-not-add-a-new-movie-to-radarr">
<Link to="https://wiki.servarr.com/Radarr_FAQ#Why_cant_I_add_a_new_movie_to_Radarr">
{translate('CantFindMovie')}
</Link>
</div>

View File

@@ -79,11 +79,11 @@ class AddNewMovieConnector extends Component {
this.props.lookupMovie({ term });
}, 300);
}
};
}
onClearMovieLookup = () => {
this.props.clearAddMovie();
};
}
//
// Render

View File

@@ -22,11 +22,11 @@ class AddNewMovieModalContent extends Component {
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
};
}
onAddMoviePress = () => {
this.props.onAddMoviePress();
};
}
//
// Render

View File

@@ -51,7 +51,7 @@ class AddNewMovieModalContentConnector extends Component {
onInputChange = ({ name, value }) => {
this.props.setAddMovieDefault({ [name]: value });
};
}
onAddMoviePress = () => {
const {
@@ -73,7 +73,7 @@ class AddNewMovieModalContentConnector extends Component {
searchForMovie: searchForMovie.value,
tags: tags.value
});
};
}
//
// Render

View File

@@ -1,9 +1,9 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating';
import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
@@ -39,15 +39,15 @@ class AddNewMovieSearchResult extends Component {
onPress = () => {
this.setState({ isNewAddMovieModalOpen: true });
};
}
onAddMovieModalClose = () => {
this.setState({ isNewAddMovieModalOpen: false });
};
}
onExternalLinkPress = (event) => {
event.stopPropagation();
};
}
//
// Render
@@ -86,13 +86,6 @@ class AddNewMovieSearchResult extends Component {
} = this.state;
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
const posterWidth = 167;
const posterHeight = 250;
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`
};
return (
<div className={styles.searchResult}>
@@ -109,7 +102,6 @@ class AddNewMovieSearchResult extends Component {
<div className={styles.posterContainer}>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
overflow={true}
@@ -122,7 +114,7 @@ class AddNewMovieSearchResult extends Component {
monitored={monitored}
hasFile={hasFile}
status={status}
posterWidth={posterWidth}
posterWidth={167}
detailedProgressBar={true}
queueStatus={queueStatus}
queueState={queueState}
@@ -190,8 +182,8 @@ class AddNewMovieSearchResult extends Component {
<div>
<Label size={sizes.LARGE}>
<TmdbRating
ratings={ratings}
<HeartRating
rating={ratings.value}
iconSize={13}
/>
</Label>

View File

@@ -32,25 +32,25 @@ class ImportMovie extends Component {
setScrollerRef = (ref) => {
this.setState({ scroller: ref });
};
}
//
// Listeners
getSelectedIds = () => {
return getSelectedIds(this.state.selectedState, { parseIds: false });
};
}
onSelectAllChange = ({ value }) => {
// Only select non-dupes
this.setState(selectAll(this.state.selectedState, value));
};
}
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey);
});
};
}
onRemoveSelectedStateItem = (id) => {
this.setState((state) => {
@@ -62,15 +62,15 @@ class ImportMovie extends Component {
selectedState
};
});
};
}
onInputChange = ({ name, value }) => {
this.props.onInputChange(this.getSelectedIds(), name, value);
};
}
onImportPress = () => {
this.props.onImportPress(this.getSelectedIds());
};
}
//
// Render

View File

@@ -112,11 +112,11 @@ class ImportMovieConnector extends Component {
[name]: value
});
});
};
}
onImportPress = (ids) => {
this.props.dispatchImportMovie({ ids });
};
}
//
// Render

View File

@@ -83,7 +83,7 @@ class ImportMovieFooter extends Component {
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
this.props.onInputChange({ name, value });
};
}
//
// Render

View File

@@ -48,7 +48,7 @@ class ImportMovieRowConnector extends Component {
id: this.props.id,
[name]: value
});
};
}
//
// Render

View File

@@ -121,7 +121,7 @@ class ImportMovieTable extends Component {
/>
</VirtualTableRow>
);
};
}
//
// Render

View File

@@ -1,5 +1,4 @@
.container {
display: flex;
.movie {
padding: 10px 20px;
width: 100%;
@@ -7,19 +6,3 @@
background-color: $menuItemHoverBackgroundColor;
}
}
.movie {
flex: 1 0 0;
overflow: hidden;
}
.tmdbLink {
composes: link from '~Components/Link/Link.css';
margin-left: auto;
color: $textColor;
}
.tmdbLinkIcon {
margin-left: 10px;
}

View File

@@ -1,8 +1,6 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import ImportMovieTitle from './ImportMovieTitle';
import styles from './ImportMovieSearchResult.css';
@@ -13,14 +11,13 @@ class ImportMovieSearchResult extends Component {
onPress = () => {
this.props.onPress(this.props.tmdbId);
};
}
//
// Render
render() {
const {
tmdbId,
title,
year,
studio,
@@ -28,30 +25,17 @@ class ImportMovieSearchResult extends Component {
} = this.props;
return (
<div className={styles.container}>
<Link
className={styles.movie}
onPress={this.onPress}
>
<ImportMovieTitle
title={title}
year={year}
network={studio}
isExistingMovie={isExistingMovie}
/>
</Link>
<Link
className={styles.tmdbLink}
to={`https://www.themoviedb.org/movie/${tmdbId}`}
>
<Icon
className={styles.tmdbLinkIcon}
name={icons.EXTERNAL_LINK}
size={16}
/>
</Link>
</div>
<Link
className={styles.movie}
onPress={this.onPress}
>
<ImportMovieTitle
title={title}
year={year}
network={studio}
isExistingMovie={isExistingMovie}
/>
</Link>
);
}
}

View File

@@ -69,7 +69,7 @@ class ImportMovieSelectMovie extends Component {
this.setState({ isOpen: false });
this._removeListener();
}
};
}
onPress = () => {
if (this.state.isOpen) {
@@ -79,7 +79,7 @@ class ImportMovieSelectMovie extends Component {
}
this.setState({ isOpen: !this.state.isOpen });
};
}
onSearchInputChange = ({ value }) => {
if (this._movieLookupTimeout) {
@@ -91,17 +91,17 @@ class ImportMovieSelectMovie extends Component {
this.props.onSearchInputChange(value);
}, 200);
});
};
}
onRefreshPress = () => {
this.props.onSearchInputChange(this.state.term);
};
}
onMovieSelect = (tmdbId) => {
this.setState({ isOpen: false });
this.props.onMovieSelect(tmdbId);
};
}
//
// Render

View File

@@ -36,7 +36,7 @@ class ImportMovieSelectMovieConnector extends Component {
term,
topOfQueue: true
});
};
}
onMovieSelect = (tmdbId) => {
const {
@@ -48,7 +48,7 @@ class ImportMovieSelectMovieConnector extends Component {
id,
selectedMovie: _.find(items, { tmdbId })
});
};
}
//
// Render

View File

@@ -25,7 +25,7 @@ class ImportMovieRootFolderRowConnector extends Component {
onDeletePress = () => {
this.props.deleteRootFolder({ id: this.props.id });
};
}
//
// Render

View File

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

View File

@@ -1,6 +1,5 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Alert from 'Components/Alert';
import FieldSet from 'Components/FieldSet';
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
import Icon from 'Components/Icon';
@@ -55,15 +54,15 @@ class ImportMovieSelectFolder extends Component {
onAddNewRootFolderPress = () => {
this.setState({ isAddNewRootFolderModalOpen: true });
};
}
onNewRootFolderSelect = ({ value }) => {
this.props.onNewRootFolderSelect(value);
};
}
onAddRootFolderModalClose = () => {
this.setState({ isAddNewRootFolderModalOpen: false });
};
}
//
// Render
@@ -73,29 +72,23 @@ class ImportMovieSelectFolder extends Component {
isWindows,
isFetching,
isPopulated,
isSaving,
error,
saveError,
items
} = this.props;
const hasRootFolders = items.length > 0;
return (
<PageContent title={translate('ImportMovies')}>
<PageContentBody>
{
isFetching && !isPopulated ?
<LoadingIndicator /> :
null
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && error ?
!isFetching && !!error &&
<div>
{translate('UnableToLoadRootFolders')}
</div> :
null
</div>
}
{
@@ -115,7 +108,7 @@ class ImportMovieSelectFolder extends Component {
</div>
{
hasRootFolders ?
items.length > 0 ?
<div className={styles.recentFolders}>
<FieldSet legend={translate('RecentFolders')}>
<Table
@@ -138,51 +131,35 @@ class ImportMovieSelectFolder extends Component {
</TableBody>
</Table>
</FieldSet>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('ChooseAnotherFolder')}
</Button>
</div> :
null
<div className={styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{translate('StartImport')}
</Button>
</div>
}
{
!isSaving && saveError ?
<Alert
className={styles.addErrorAlert}
kind={kinds.DANGER}
>
{translate('UnableToAddRootFolder')}
<ul>
{
saveError.responseJSON.map((e, index) => {
return (
<li key={index}>
{e.errorMessage}
</li>
);
})
}
</ul>
</Alert> :
null
}
<div className={hasRootFolders ? undefined : styles.startImport}>
<Button
kind={kinds.PRIMARY}
size={sizes.LARGE}
onPress={this.onAddNewRootFolderPress}
>
<Icon
className={styles.importButtonIcon}
name={icons.DRIVE}
/>
{
hasRootFolders ?
translate('ChooseAnotherFolder') :
translate('StartImport')
}
</Button>
</div>
<FileBrowserModal
isOpen={this.state.isAddNewRootFolderModalOpen}
name="rootFolderPath"
@@ -202,9 +179,7 @@ ImportMovieSelectFolder.propTypes = {
isWindows: PropTypes.bool.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
error: PropTypes.object,
saveError: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
onNewRootFolderSelect: PropTypes.func.isRequired,
onDeleteRootFolderPress: PropTypes.func.isRequired

View File

@@ -58,11 +58,11 @@ class ImportMovieSelectFolderConnector extends Component {
onNewRootFolderSelect = (path) => {
this.props.addRootFolder({ path });
};
}
onDeleteRootFolderPress = (id) => {
this.props.deleteRootFolder({ id });
};
}
//
// Render

View File

@@ -8,7 +8,7 @@ import AppRoutes from './AppRoutes';
function App({ store, history }) {
return (
<DocumentTitle title={window.Radarr.instanceName}>
<DocumentTitle title="Radarr">
<Provider store={store}>
<ConnectedRouter history={history}>
<PageConnector>

View File

@@ -1,13 +1,12 @@
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 AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import CollectionConnector from 'Collection/CollectionConnector';
import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
@@ -73,11 +72,6 @@ function AppRoutes(props) {
component={AddNewMovieConnector}
/>
<Route
path="/collections"
component={CollectionConnector}
/>
<Route
path="/add/import"
component={ImportMovies}
@@ -117,8 +111,8 @@ function AppRoutes(props) {
/>
<Route
path="/activity/blocklist"
component={BlocklistConnector}
path="/activity/blacklist"
component={BlacklistConnector}
/>
{/*

View File

@@ -54,24 +54,20 @@
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
}
.queue {
composes: queue from '~Calendar/Events/CalendarEvent.css';
.downloading {
composes: downloading from '~Calendar/Events/CalendarEvent.css';
}
.unmonitored {
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
}
.missingUnmonitored {
composes: missingUnmonitored from '~Calendar/Events/CalendarEvent.css';
.missing {
composes: missing from '~Calendar/Events/CalendarEvent.css';
}
.missingMonitored {
composes: missingMonitored from '~Calendar/Events/CalendarEvent.css';
}
.continuing {
composes: continuing from '~Calendar/Events/CalendarEvent.css';
.unreleased {
composes: unreleased from '~Calendar/Events/CalendarEvent.css';
}
@media only screen and (max-width: $breakpointSmall) {

View File

@@ -3,10 +3,10 @@ import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CalendarEventQueueDetails from 'Calendar/Events/CalendarEventQueueDetails';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate';
import styles from './AgendaEvent.css';
@@ -27,11 +27,11 @@ class AgendaEvent extends Component {
onPress = () => {
this.setState({ isDetailsModalOpen: true });
};
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
};
}
//
// Render
@@ -82,7 +82,7 @@ class AgendaEvent extends Component {
startTime = moment(startTime);
const downloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', downloading);
const statusStyle = getStatusStyle(hasFile, downloading, isAvailable, isMonitored);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;

View File

@@ -119,43 +119,43 @@ class CalendarConnector extends Component {
this.props.fetchQueueDetails({ time, view });
this.props.fetchCalendar({ time, view });
};
}
scheduleUpdate = () => {
this.clearUpdateTimeout();
this.updateTimeoutId = setTimeout(this.updateCalendar, UPDATE_DELAY);
};
}
clearUpdateTimeout = () => {
if (this.updateTimeoutId) {
clearTimeout(this.updateTimeoutId);
}
};
}
updateCalendar = () => {
this.props.gotoCalendarToday();
this.scheduleUpdate();
};
}
//
// Listeners
onCalendarViewChange = (view) => {
this.props.setCalendarView({ view });
};
}
onTodayPress = () => {
this.props.gotoCalendarToday();
};
}
onPreviousPress = () => {
this.props.gotoCalendarPreviousRange();
};
}
onNextPress = () => {
this.props.gotoCalendarNextRange();
};
}
//
// Render

View File

@@ -44,23 +44,23 @@ class CalendarPage extends Component {
const days = Math.max(3, Math.min(7, Math.floor(width / MINIMUM_DAY_WIDTH)));
this.props.onDaysCountChange(days);
};
}
onGetCalendarLinkPress = () => {
this.setState({ isCalendarLinkModalOpen: true });
};
}
onGetCalendarLinkModalClose = () => {
this.setState({ isCalendarLinkModalOpen: false });
};
}
onOptionsPress = () => {
this.setState({ isOptionsModalOpen: true });
};
}
onOptionsModalClose = () => {
this.setState({ isOptionsModalOpen: false });
};
}
onSearchMissingPress = () => {
const {
@@ -69,7 +69,7 @@ class CalendarPage extends Component {
} = this.props;
onSearchMissingPress(missingMovieIds);
};
}
//
// Render

View File

@@ -60,20 +60,20 @@ class CalendarDays extends Component {
this.setState({ todaysDate: todaysDate.toISOString() });
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
};
}
clearUpdateTimeout = () => {
if (this.updateTimeoutId) {
clearTimeout(this.updateTimeoutId);
}
};
}
//
// Listeners
onEventModalOpenToggle = (isEventModalOpen) => {
this.setState({ isEventModalOpen });
};
}
onTouchStart = (event) => {
const touches = event.touches;
@@ -92,7 +92,7 @@ class CalendarDays extends Component {
}
this._touchStart = touchStart;
};
}
onTouchEnd = (event) => {
const touches = event.changedTouches;
@@ -109,17 +109,17 @@ class CalendarDays extends Component {
}
this._touchStart = null;
};
}
onTouchCancel = (event) => {
this._touchStart = null;
};
}
onTouchMove = (event) => {
if (!this._touchStart) {
return;
}
};
}
//
// Render

View File

@@ -47,13 +47,13 @@ class DaysOfWeek extends Component {
});
this.updateTimeoutId = setTimeout(this.scheduleUpdate, diff);
};
}
clearUpdateTimeout = () => {
if (this.updateTimeoutId) {
clearTimeout(this.updateTimeoutId);
}
};
}
//
// Render

View File

@@ -60,30 +60,39 @@
}
}
.queue {
.downloading {
border-left-color: $purple !important;
}
.unmonitored {
border-left-color: $gray !important;
}
.missingUnmonitored {
border-left-color: $warningColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
.missingMonitored {
border-left-color: $dangerColor !important;
.onAir {
border-left-color: $warningColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
.continuing {
border-left-color: $primaryColor !important;
.missing {
border-left-color: $dangerColor !important;
&:global(.colorImpaired) {
border-left-color: color($dangerColor saturation(+15%)) !important;
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
.unreleased {
border-left-color: $primaryColor !important;
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}

View File

@@ -2,10 +2,10 @@ import classNames from 'classnames';
import moment from 'moment';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import getStatusStyle from 'Calendar/getStatusStyle';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import { icons, kinds } from 'Helpers/Props';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate';
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
import styles from './CalendarEvent.css';
@@ -38,20 +38,20 @@ class CalendarEvent extends Component {
const isDownloading = !!(queueItem || grabbed);
const isMonitored = monitored;
const statusStyle = getStatusStyle(null, isMonitored, hasFile, isAvailable, 'style', isDownloading);
const statusStyle = getStatusStyle(hasFile, isDownloading, isAvailable, isMonitored);
const joinedGenres = genres.slice(0, 2).join(', ');
const link = `/movie/${titleSlug}`;
const eventType = [];
if (inCinemas && moment(date).isSame(moment(inCinemas), 'day')) {
if (moment(date).isSame(moment(inCinemas), 'day')) {
eventType.push('Cinemas');
}
if (physicalRelease && moment(date).isSame(moment(physicalRelease), 'day')) {
if (moment(date).isSame(moment(physicalRelease), 'day')) {
eventType.push('Physical');
}
if (digitalRelease && moment(date).isSame(moment(digitalRelease), 'day')) {
if (moment(date).isSame(moment(digitalRelease), 'day')) {
eventType.push('Digital');
}

View File

@@ -71,7 +71,7 @@ class CalendarHeader extends Component {
this.setState({ view }, () => {
this.props.onViewChange(view);
});
};
}
//
// Render

View File

@@ -41,19 +41,19 @@ class CalendarHeaderConnector extends Component {
onViewChange = (view) => {
this.props.setCalendarView({ view });
};
}
onTodayPress = () => {
this.props.gotoCalendarToday();
};
}
onPreviousPress = () => {
this.props.gotoCalendarPreviousRange();
};
}
onNextPress = () => {
this.props.gotoCalendarNextRange();
};
}
//
// Render

View File

@@ -12,7 +12,7 @@ class CalendarHeaderViewButton extends Component {
onPress = () => {
this.props.onPress(this.props.view);
};
}
//
// Render

View File

@@ -69,7 +69,7 @@ class CalendarOptionsModalContent extends Component {
} = this.props;
dispatchSetCalendarOption({ [name]: value });
};
}
onGlobalInputChange = ({ name, value }) => {
const {
@@ -81,11 +81,11 @@ class CalendarOptionsModalContent extends Component {
this.setState(setting, () => {
dispatchSaveUISettings(setting);
});
};
}
onLinkFocus = (event) => {
event.target.select();
};
}
//
// Render

View File

@@ -0,0 +1,23 @@
function getStatusStyle(hasFile, downloading, isAvailable, isMonitored) {
if (hasFile) {
return 'downloaded';
}
if (downloading) {
return 'downloading';
}
if (!isMonitored) {
return 'unmonitored';
}
if (isAvailable && !hasFile) {
return 'missing';
}
return 'unreleased';
}
export default getStatusStyle;

View File

@@ -84,11 +84,11 @@ class CalendarLinkModalContent extends Component {
[name]: value,
...urls
});
};
}
onLinkFocus = (event) => {
event.target.select();
};
}
//
// Render

View File

@@ -1,31 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import AddNewCollectionMovieModalContentConnector from './AddNewCollectionMovieModalContentConnector';
function AddNewCollectionMovieModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<AddNewCollectionMovieModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
AddNewCollectionMovieModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default AddNewCollectionMovieModal;

View File

@@ -1,68 +0,0 @@
.container {
display: flex;
}
.year {
margin-left: 5px;
color: $disabledColor;
}
.poster {
flex: 0 0 170px;
margin-right: 20px;
height: 250px;
}
.info {
flex-grow: 1;
}
.overview {
margin-bottom: 30px;
}
.labelIcon {
margin-left: 8px;
}
.searchForMissingMovieLabelContainer {
display: flex;
margin-top: 2px;
}
.searchForMissingMovieLabel {
margin-right: 8px;
font-weight: normal;
}
.searchForMissingMovieContainer {
composes: container from '~Components/Form/CheckInput.css';
flex: 0 1 0;
}
.searchForMissingMovieInput {
composes: input from '~Components/Form/CheckInput.css';
margin-top: 0;
}
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
}
.addButton {
@add-mixin truncate;
composes: button from '~Components/Link/SpinnerButton.css';
}
@media only screen and (max-width: $breakpointSmall) {
.modalFooter {
display: block;
text-align: center;
}
.addButton {
margin-top: 10px;
}
}

View File

@@ -1,204 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import CheckInput from 'Components/Form/CheckInput';
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 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 MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
import styles from './AddNewCollectionMovieModalContent.css';
class AddNewCollectionMovieModalContent extends Component {
//
// Listeners
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
};
onAddMoviePress = () => {
this.props.onAddMoviePress();
};
//
// Render
render() {
const {
title,
year,
overview,
images,
isAdding,
folder,
tags,
isSmallScreen,
isWindows,
onModalClose,
onInputChange,
rootFolderPath,
monitor,
qualityProfileId,
minimumAvailability,
searchForMovie
} = this.props;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{title}
{
!title.contains(year) && !!year &&
<span className={styles.year}>({year})</span>
}
</ModalHeader>
<ModalBody>
<div className={styles.container}>
{
!isSmallScreen &&
<div className={styles.poster}>
<MoviePoster
className={styles.poster}
images={images}
size={250}
/>
</div>
}
<div className={styles.info}>
<div className={styles.overview}>
{overview}
</div>
<Form>
<FormGroup>
<FormLabel>{translate('RootFolder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
valueOptions={{
movieFolder: folder,
isWindows
}}
selectedValueOptions={{
movieFolder: folder,
isWindows
}}
helpText={translate('SubfolderWillBeCreatedAutomaticallyInterp', [folder])}
onChange={onInputChange}
{...rootFolderPath}
/>
</FormGroup>
<FormGroup>
<FormLabel>
{translate('Monitor')}
</FormLabel>
<FormInputGroup
type={inputTypes.MOVIE_MONITORED_SELECT}
name="monitor"
onChange={onInputChange}
{...monitor}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
onChange={onInputChange}
{...minimumAvailability}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
onChange={this.onQualityProfileIdChange}
{...qualityProfileId}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Tags')}</FormLabel>
<FormInputGroup
type={inputTypes.TAG}
name="tags"
onChange={onInputChange}
{...tags}
/>
</FormGroup>
</Form>
</div>
</div>
</ModalBody>
<ModalFooter className={styles.modalFooter}>
<label className={styles.searchForMissingMovieLabelContainer}>
<span className={styles.searchForMissingMovieLabel}>
{translate('StartSearchForMissingMovie')}
</span>
<CheckInput
containerClassName={styles.searchForMissingMovieContainer}
className={styles.searchForMissingMovieInput}
name="searchForMovie"
onChange={onInputChange}
{...searchForMovie}
/>
</label>
<SpinnerButton
className={styles.addButton}
kind={kinds.SUCCESS}
isSpinning={isAdding}
onPress={this.onAddMoviePress}
>
{translate('AddMovie')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
AddNewCollectionMovieModalContent.propTypes = {
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
overview: PropTypes.string,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
isAdding: PropTypes.bool.isRequired,
addError: PropTypes.object,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired,
tags: PropTypes.object.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isWindows: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,
onInputChange: PropTypes.func.isRequired,
onAddMoviePress: PropTypes.func.isRequired
};
export default AddNewCollectionMovieModalContent;

View File

@@ -1,121 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { addMovie, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import AddNewMovieModalContent from './AddNewCollectionMovieModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movieCollections,
createCollectionSelector(),
createDimensionsSelector(),
createSystemStatusSelector(),
(discoverMovieState, collection, dimensions, systemStatus) => {
const {
isAdding,
addError,
pendingChanges
} = discoverMovieState;
const collectionDefaults = {
rootFolderPath: collection.rootFolderPath,
monitor: 'movieOnly',
qualityProfileId: collection.qualityProfileId,
minimumAvailability: collection.minimumAvailability,
searchForMovie: collection.searchOnAdd,
tags: []
};
const {
settings,
validationErrors,
validationWarnings
} = selectSettings(collectionDefaults, pendingChanges, addError);
return {
isAdding,
addError,
isSmallScreen: dimensions.isSmallScreen,
validationErrors,
validationWarnings,
isWindows: systemStatus.isWindows,
...settings
};
}
);
}
const mapDispatchToProps = {
addMovie,
setMovieCollectionValue
};
class AddNewCollectionMovieModalContentConnector extends Component {
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.setMovieCollectionValue({ name, value });
};
onAddMoviePress = () => {
const {
tmdbId,
title,
rootFolderPath,
monitor,
qualityProfileId,
minimumAvailability,
searchForMovie,
tags
} = this.props;
this.props.addMovie({
tmdbId,
title,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileId: qualityProfileId.value,
minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value,
tags: tags.value
});
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<AddNewMovieModalContent
{...this.props}
onInputChange={this.onInputChange}
onAddMoviePress={this.onAddMoviePress}
/>
);
}
}
AddNewCollectionMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired,
qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired,
onModalClose: PropTypes.func.isRequired,
addMovie: PropTypes.func.isRequired,
setMovieCollectionValue: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewCollectionMovieModalContentConnector);

View File

@@ -1,403 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
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 { align, icons, sortDirections } from 'Helpers/Props';
import styles from 'Movie/Index/MovieIndex.css';
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 CollectionFooter from './CollectionFooter';
import CollectionFilterMenu from './Menus/CollectionFilterMenu';
import CollectionSortMenu from './Menus/CollectionSortMenu';
import NoCollection from './NoCollection';
import CollectionOverviewsConnector from './Overview/CollectionOverviewsConnector';
import CollectionOverviewOptionsModal from './Overview/Options/CollectionOverviewOptionsModal';
function getViewComponent(view) {
return CollectionOverviewsConnector;
}
class Collection extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
scroller: null,
jumpBarItems: { order: [] },
jumpToCharacter: null,
isPosterOptionsModalOpen: false,
isOverviewOptionsModalOpen: false,
isConfirmSearchModalOpen: false,
searchType: null,
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((collection) => {
const isItemSelected = selectedState[collection.id];
if (isItemSelected) {
newSelectedState[collection.id] = isItemSelected;
} else {
newSelectedState[collection.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
} = this.props;
// Reset if not sorting by sortTitle
if (sortKey !== 'sortTitle') {
this.setState({ jumpBarItems: { order: [] } });
return;
}
const characters = _.reduce(items, (acc, item) => {
let char = item.sortTitle.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
onOverviewOptionsPress = () => {
this.setState({ isOverviewOptionsModalOpen: true });
};
onOverviewOptionsModalClose = () => {
this.setState({ isOverviewOptionsModalOpen: false });
};
onJumpBarItemPress = (jumpToCharacter) => {
this.setState({ jumpToCharacter });
};
onSelectAllChange = ({ value }) => {
this.setState(selectAll(this.state.selectedState, value));
};
onSelectAllPress = () => {
this.onSelectAllChange({ value: !this.state.allSelected });
};
onRefreshMovieCollectionsPress = () => {
this.props.onRefreshMovieCollectionsPress();
};
onSelectedChange = ({ id, value, shiftKey = false }) => {
this.setState((state) => {
return toggleSelected(state, this.props.items, id, value, shiftKey, 'id');
});
};
onUpdateSelectedPress = (changes) => {
this.props.onUpdateSelectedPress({
collectionIds: this.getSelectedIds(),
...changes
});
};
//
// Render
render() {
const {
isFetching,
isPopulated,
error,
totalItems,
items,
selectedFilterKey,
filters,
customFilters,
sortKey,
sortDirection,
view,
onSortSelect,
onFilterSelect,
onScroll,
isRefreshingCollections,
isSaving,
isAdding,
...otherProps
} = this.props;
const {
scroller,
jumpBarItems,
jumpToCharacter,
isOverviewOptionsModalOpen,
selectedState,
allSelected,
allUnselected
} = this.state;
const selectedMovieIds = this.getSelectedIds();
const ViewComponent = getViewComponent(view);
const isLoaded = !!(!error && isPopulated && items.length && scroller);
const hasNoCollection = !totalItems;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection>
<PageToolbarButton
label={translate('RefreshCollections')}
iconName={icons.REFRESH}
isSpinning={isRefreshingCollections}
isDisabled={hasNoCollection}
onPress={this.onRefreshMovieCollectionsPress}
/>
<PageToolbarButton
label={allSelected ? translate('UnselectAll') : translate('SelectAll')}
iconName={icons.CHECK_SQUARE}
isDisabled={hasNoCollection}
onPress={this.onSelectAllPress}
/>
</PageToolbarSection>
<PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
{
view === 'overview' ?
<PageToolbarButton
label={translate('Options')}
iconName={icons.OVERVIEW}
onPress={this.onOverviewOptionsPress}
/> :
null
}
{
(view === 'posters' || view === 'overview') &&
<PageToolbarSeparator />
}
<CollectionSortMenu
sortKey={sortKey}
sortDirection={sortDirection}
isDisabled={hasNoCollection}
onSortSelect={onSortSelect}
/>
<CollectionFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
isDisabled={hasNoCollection}
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>
{translate('UnableToLoadCollections')}
</div>
}
{
isLoaded &&
<div className={styles.contentBodyContainer}>
<ViewComponent
scroller={scroller}
items={items}
filters={filters}
sortKey={sortKey}
sortDirection={sortDirection}
jumpToCharacter={jumpToCharacter}
allSelected={allSelected}
allUnselected={allUnselected}
onSelectedChange={this.onSelectedChange}
onSelectAllChange={this.onSelectAllChange}
selectedState={selectedState}
{...otherProps}
/>
</div>
}
{
!error && isPopulated && !items.length &&
<NoCollection totalItems={totalItems} />
}
</PageContentBody>
{
isLoaded && !!jumpBarItems.order.length &&
<PageJumpBar
items={jumpBarItems}
onItemPress={this.onJumpBarItemPress}
/>
}
</div>
{
isLoaded &&
<CollectionFooter
selectedIds={selectedMovieIds}
isSaving={isSaving}
isAdding={isAdding}
onUpdateSelectedPress={this.onUpdateSelectedPress}
/>
}
<CollectionOverviewOptionsModal
isOpen={isOverviewOptionsModalOpen}
onModalClose={this.onOverviewOptionsModalClose}
/>
</PageContent>
);
}
}
Collection.propTypes = {
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
isAdding: PropTypes.bool.isRequired,
error: PropTypes.object,
totalItems: PropTypes.number.isRequired,
items: 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,
isRefreshingCollections: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired,
onScroll: PropTypes.func.isRequired,
onUpdateSelectedPress: PropTypes.func.isRequired,
onRefreshMovieCollectionsPress: PropTypes.func.isRequired
};
export default Collection;

View File

@@ -1,108 +0,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 { executeCommand } from 'Store/Actions/commandActions';
import { saveMovieCollections, setMovieCollectionsFilter, setMovieCollectionsSort } from 'Store/Actions/movieCollectionActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import scrollPositions from 'Store/scrollPositions';
import createCollectionClientSideCollectionItemsSelector from 'Store/Selectors/createCollectionClientSideCollectionItemsSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import Collection from './Collection';
function createMapStateToProps() {
return createSelector(
createCollectionClientSideCollectionItemsSelector('movieCollections'),
createCommandExecutingSelector(commandNames.REFRESH_COLLECTIONS),
createDimensionsSelector(),
(
collections,
isRefreshingCollections,
dimensionsState
) => {
return {
...collections,
isRefreshingCollections,
isSmallScreen: dimensionsState.isSmallScreen
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
dispatchFetchRootFolders() {
dispatch(fetchRootFolders());
},
onUpdateSelectedPress(payload) {
dispatch(saveMovieCollections(payload));
},
onSortSelect(sortKey) {
dispatch(setMovieCollectionsSort({ sortKey }));
},
onFilterSelect(selectedFilterKey) {
dispatch(setMovieCollectionsFilter({ selectedFilterKey }));
},
onRefreshMovieCollectionsPress() {
dispatch(executeCommand({
name: commandNames.REFRESH_COLLECTIONS
}));
}
};
}
class CollectionConnector extends Component {
//
// Lifecycle
componentDidMount() {
registerPagePopulator(this.repopulate);
this.props.dispatchFetchRootFolders();
}
componentWillUnmount() {
unregisterPagePopulator(this.repopulate);
}
//
// Listeners
onScroll = ({ scrollTop }) => {
scrollPositions.movieCollections = scrollTop;
};
onUpdateSelectedPress = (payload) => {
this.props.onUpdateSelectedPress(payload);
};
//
// Render
render() {
return (
<Collection
{...this.props}
onViewSelect={this.onViewSelect}
onScroll={this.onScroll}
onUpdateSelectedPress={this.onUpdateSelectedPress}
/>
);
}
}
CollectionConnector.propTypes = {
isSmallScreen: PropTypes.bool.isRequired,
view: PropTypes.string.isRequired,
onUpdateSelectedPress: PropTypes.func.isRequired,
dispatchFetchRootFolders: PropTypes.func.isRequired
};
export default withScrollPosition(
connect(createMapStateToProps, createMapDispatchToProps)(CollectionConnector),
'movieCollections'
);

View File

@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setMovieCollectionsFilter } from 'Store/Actions/movieCollectionActions';
function createMapStateToProps() {
return createSelector(
(state) => state.movieCollections.items,
(state) => state.movieCollections.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'movieCollections'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setMovieCollectionsFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

View File

@@ -1,56 +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;
}
.addSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-right: 10px;
height: 35px;
}
.excludeSelectedButton {
composes: button from '~Components/Link/SpinnerButton.css';
margin-left: 25px;
height: 35px;
}
@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;
}
.selectedMovieLabel {
text-align: left;
}
}

View File

@@ -1,231 +0,0 @@
import _ from 'lodash';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import AvailabilitySelectInput from 'Components/Form/AvailabilitySelectInput';
import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSelectInputConnector';
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import CollectionFooterLabel from './CollectionFooterLabel';
import styles from './CollectionFooter.css';
const NO_CHANGE = 'noChange';
class CollectionFooter extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
monitor: NO_CHANGE,
monitored: NO_CHANGE,
qualityProfileId: NO_CHANGE,
minimumAvailability: NO_CHANGE,
rootFolderPath: NO_CHANGE,
destinationRootFolder: null
};
}
componentDidUpdate(prevProps) {
const {
isSaving,
saveError
} = this.props;
const newState = {};
if (prevProps.isSaving && !isSaving && !saveError) {
this.setState({
monitored: NO_CHANGE,
monitor: NO_CHANGE,
qualityProfileId: NO_CHANGE,
rootFolderPath: NO_CHANGE,
minimumAvailability: NO_CHANGE
});
}
if (!_.isEmpty(newState)) {
this.setState(newState);
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.setState({ [name]: value });
};
onUpdateSelectedPress = () => {
const {
monitor,
monitored,
qualityProfileId,
minimumAvailability
} = this.state;
const changes = {};
if (monitored !== NO_CHANGE) {
changes.monitored = monitored === 'monitored';
}
if (monitor !== NO_CHANGE) {
changes.monitor = monitor;
}
if (qualityProfileId !== NO_CHANGE) {
changes.qualityProfileId = qualityProfileId;
}
if (minimumAvailability !== NO_CHANGE) {
changes.minimumAvailability = minimumAvailability;
}
this.props.onUpdateSelectedPress(changes);
};
//
// Render
render() {
const {
selectedIds,
isSaving
} = this.props;
const {
monitored,
monitor,
qualityProfileId,
minimumAvailability,
rootFolderPath
} = this.state;
const monitoredOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'monitored', value: translate('Monitored') },
{ key: 'unmonitored', value: translate('Unmonitored') }
];
const selectedCount = selectedIds.length;
return (
<PageContentFooter>
<div className={styles.inputContainer}>
<CollectionFooterLabel
label={translate('MonitorCollection')}
isSaving={isSaving}
/>
<SelectInput
name="monitored"
value={monitored}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<CollectionFooterLabel
label={translate('MonitorMovies')}
isSaving={isSaving}
/>
<SelectInput
name="monitor"
value={monitor}
values={monitoredOptions}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<CollectionFooterLabel
label={translate('QualityProfile')}
isSaving={isSaving && qualityProfileId !== NO_CHANGE}
/>
<QualityProfileSelectInputConnector
name="qualityProfileId"
value={qualityProfileId}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<CollectionFooterLabel
label={translate('MinimumAvailability')}
isSaving={isSaving && minimumAvailability !== NO_CHANGE}
/>
<AvailabilitySelectInput
name="minimumAvailability"
value={minimumAvailability}
includeNoChange={true}
isDisabled={!selectedCount}
onChange={this.onInputChange}
/>
</div>
<div className={styles.inputContainer}>
<CollectionFooterLabel
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 className={styles.buttonContainer}>
<div className={styles.buttonContainerContent}>
<CollectionFooterLabel
label={translate('CollectionsSelectedInterp', [selectedCount])}
isSaving={false}
/>
<div className={styles.buttons}>
<div>
<SpinnerButton
className={styles.addSelectedButton}
kind={kinds.PRIMARY}
isSpinning={isSaving}
isDisabled={!selectedCount || isSaving}
onPress={this.onUpdateSelectedPress}
>
{translate('UpdateSelected')}
</SpinnerButton>
</div>
</div>
</div>
</div>
</PageContentFooter>
);
}
}
CollectionFooter.propTypes = {
selectedIds: PropTypes.arrayOf(PropTypes.number).isRequired,
isAdding: PropTypes.bool.isRequired,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
onUpdateSelectedPress: PropTypes.func.isRequired
};
export default CollectionFooter;

View File

@@ -1,8 +0,0 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}

View File

@@ -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 './CollectionFooterLabel.css';
function CollectionFooterLabel(props) {
const {
className,
label,
isSaving
} = props;
return (
<div className={className}>
{label}
{
isSaving &&
<SpinnerIcon
className={styles.savingIcon}
name={icons.SPINNER}
isSpinning={true}
/>
}
</div>
);
}
CollectionFooterLabel.propTypes = {
className: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
isSaving: PropTypes.bool.isRequired
};
CollectionFooterLabel.defaultProps = {
className: styles.label
};
export default CollectionFooterLabel;

View File

@@ -1,75 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
function createMapStateToProps() {
return createSelector(
createCollectionSelector(),
createAllMoviesSelector(),
(
collection,
allMovies
) => {
// If a movie is deleted this selector may fire before the parent
// selecors, which will result in an undefined movie, if that happens
// we want to return early here and again in the render function to avoid
// trying to show a movie that has no information available.
if (!collection) {
return {};
}
let allGenres = [];
let libraryMovies = 0;
collection.movies.forEach((movie) => {
allGenres = allGenres.concat(movie.genres);
if (allMovies.find((libraryMovie) => libraryMovie.tmdbId === movie.tmdbId)) {
libraryMovies++;
}
});
return {
...collection,
genres: Array.from(new Set(allGenres)).slice(0, 3),
missingMovies: collection.movies.length - libraryMovies
};
}
);
}
class CollectionItemConnector extends Component {
//
// Render
render() {
const {
id,
component: ItemComponent,
...otherProps
} = this.props;
if (!id) {
return null;
}
return (
<ItemComponent
{...otherProps}
id={id}
/>
);
}
}
CollectionItemConnector.propTypes = {
id: PropTypes.number,
component: PropTypes.elementType.isRequired
};
export default connect(createMapStateToProps)(CollectionItemConnector);

View File

@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import EditCollectionModalContentConnector from './EditCollectionModalContentConnector';
function EditCollectionModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<EditCollectionModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
EditCollectionModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditCollectionModal;

View File

@@ -1,39 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { clearPendingChanges } from 'Store/Actions/baseActions';
import EditCollectionModal from './EditCollectionModal';
const mapDispatchToProps = {
clearPendingChanges
};
class EditCollectionModalConnector extends Component {
//
// Listeners
onModalClose = () => {
this.props.clearPendingChanges({ section: 'movieCollections' });
this.props.onModalClose();
};
//
// Render
render() {
return (
<EditCollectionModal
{...this.props}
onModalClose={this.onModalClose}
/>
);
}
}
EditCollectionModalConnector.propTypes = {
onModalClose: PropTypes.func.isRequired,
clearPendingChanges: PropTypes.func.isRequired
};
export default connect(undefined, mapDispatchToProps)(EditCollectionModalConnector);

View File

@@ -1,17 +0,0 @@
.container {
display: flex;
}
.poster {
flex: 0 0 170px;
margin-right: 20px;
height: 250px;
}
.info {
flex-grow: 1;
}
.overview {
margin-bottom: 30px;
}

View File

@@ -1,178 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
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 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 } from 'Helpers/Props';
import MoviePoster from 'Movie/MoviePoster';
import translate from 'Utilities/String/translate';
import styles from './EditCollectionModalContent.css';
class EditCollectionModalContent extends Component {
//
// Listeners
onSavePress = () => {
const {
onSavePress
} = this.props;
onSavePress(false);
};
//
// Render
render() {
const {
title,
images,
overview,
item,
isSaving,
onInputChange,
onModalClose,
isSmallScreen,
...otherProps
} = this.props;
const {
monitored,
qualityProfileId,
minimumAvailability,
// Id,
rootFolderPath,
searchOnAdd
} = item;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Edit')} - {title}
</ModalHeader>
<ModalBody>
<div className={styles.container}>
{
!isSmallScreen &&
<div className={styles.poster}>
<MoviePoster
className={styles.poster}
images={images}
size={250}
/>
</div>
}
<div className={styles.info}>
<div className={styles.overview}>
{overview}
</div>
<Form
{...otherProps}
>
<FormGroup>
<FormLabel>{translate('Monitored')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="monitored"
helpText={translate('MonitoredCollectionHelpText')}
{...monitored}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('MinimumAvailability')}</FormLabel>
<FormInputGroup
type={inputTypes.AVAILABILITY_SELECT}
name="minimumAvailability"
{...minimumAvailability}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('QualityProfile')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileId"
{...qualityProfileId}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('Folder')}</FormLabel>
<FormInputGroup
type={inputTypes.ROOT_FOLDER_SELECT}
name="rootFolderPath"
{...rootFolderPath}
includeMissingValue={true}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('SearchOnAdd')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="searchOnAdd"
helpText={translate('SearchOnAddCollectionHelpText')}
{...searchOnAdd}
onChange={onInputChange}
/>
</FormGroup>
</Form>
</div>
</div>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Cancel')}
</Button>
<SpinnerButton
isSpinning={isSaving}
onPress={this.onSavePress}
>
{translate('Save')}
</SpinnerButton>
</ModalFooter>
</ModalContent>
);
}
}
EditCollectionModalContent.propTypes = {
collectionId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
item: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
isPathChanging: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onInputChange: PropTypes.func.isRequired,
onSavePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default EditCollectionModalContent;

View File

@@ -1,119 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { saveMovieCollection, setMovieCollectionValue } from 'Store/Actions/movieCollectionActions';
import createCollectionSelector from 'Store/Selectors/createCollectionSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import selectSettings from 'Store/Selectors/selectSettings';
import EditCollectionModalContent from './EditCollectionModalContent';
function createIsPathChangingSelector() {
return createSelector(
(state) => state.movieCollections.pendingChanges,
createCollectionSelector(),
(pendingChanges, collection) => {
const rootFolderPath = pendingChanges.rootFolderPath;
if (rootFolderPath == null) {
return false;
}
return collection.rootFolderPath !== rootFolderPath;
}
);
}
function createMapStateToProps() {
return createSelector(
(state) => state.movieCollections,
createCollectionSelector(),
createIsPathChangingSelector(),
createDimensionsSelector(),
(moviesState, collection, isPathChanging, dimensions) => {
const {
isSaving,
saveError,
pendingChanges
} = moviesState;
const movieSettings = {
monitored: collection.monitored,
qualityProfileId: collection.qualityProfileId,
minimumAvailability: collection.minimumAvailability,
rootFolderPath: collection.rootFolderPath,
searchOnAdd: collection.searchOnAdd
};
const settings = selectSettings(movieSettings, pendingChanges, saveError);
return {
title: collection.title,
images: collection.images,
overview: collection.overview,
isSaving,
saveError,
isPathChanging,
originalPath: collection.path,
item: settings.settings,
isSmallScreen: dimensions.isSmallScreen,
...settings
};
}
);
}
const mapDispatchToProps = {
dispatchSetMovieCollectionValue: setMovieCollectionValue,
dispatchSaveMovieCollection: saveMovieCollection
};
class EditCollectionModalContentConnector extends Component {
//
// Lifecycle
componentDidUpdate(prevProps, prevState) {
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
this.props.onModalClose();
}
}
//
// Listeners
onInputChange = ({ name, value }) => {
this.props.dispatchSetMovieCollectionValue({ name, value });
};
onSavePress = () => {
this.props.dispatchSaveMovieCollection({
id: this.props.collectionId
});
};
//
// Render
render() {
return (
<EditCollectionModalContent
{...this.props}
onInputChange={this.onInputChange}
onSavePress={this.onSavePress}
onMoveMoviePress={this.onMoveMoviePress}
/>
);
}
}
EditCollectionModalContentConnector.propTypes = {
collectionId: PropTypes.number,
isSaving: PropTypes.bool.isRequired,
saveError: PropTypes.object,
dispatchSetMovieCollectionValue: PropTypes.func.isRequired,
dispatchSaveMovieCollection: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(EditCollectionModalContentConnector);

View File

@@ -1,41 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import CollectionFilterModalConnector from 'Collection/CollectionFilterModalConnector';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
function CollectionFilterMenu(props) {
const {
selectedFilterKey,
filters,
customFilters,
isDisabled,
onFilterSelect
} = props;
return (
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={isDisabled}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
filterModalConnectorComponent={CollectionFilterModalConnector}
onFilterSelect={onFilterSelect}
/>
);
}
CollectionFilterMenu.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
};
CollectionFilterMenu.defaultProps = {
showCustomFilters: false
};
export default CollectionFilterMenu;

View File

@@ -1,43 +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';
import translate from 'Utilities/String/translate';
function CollectionSortMenu(props) {
const {
sortKey,
sortDirection,
isDisabled,
onSortSelect
} = props;
return (
<SortMenu
isDisabled={isDisabled}
alignMenu={align.RIGHT}
>
<MenuContent>
<SortMenuItem
name="sortTitle"
sortKey={sortKey}
sortDirection={sortDirection}
onPress={onSortSelect}
>
{translate('Title')}
</SortMenuItem>
</MenuContent>
</SortMenu>
);
}
CollectionSortMenu.propTypes = {
sortKey: PropTypes.string,
sortDirection: PropTypes.oneOf(sortDirections.all),
isDisabled: PropTypes.bool.isRequired,
onSortSelect: PropTypes.func.isRequired
};
export default CollectionSortMenu;

View File

@@ -1,11 +0,0 @@
.message {
margin-top: 10px;
margin-bottom: 30px;
text-align: center;
font-size: 20px;
}
.buttonContainer {
margin-top: 20px;
text-align: center;
}

View File

@@ -1,52 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoCollection.css';
function NoCollection(props) {
const { totalItems } = props;
if (totalItems > 0) {
return (
<div>
<div className={styles.message}>
{translate('AllCollectionsHiddenDueToFilter')}
</div>
</div>
);
}
return (
<div>
<div className={styles.message}>
{translate('NoCollections')}
</div>
<div className={styles.buttonContainer}>
<Button
to="/add/import"
kind={kinds.PRIMARY}
>
{translate('ImportExistingMovies')}
</Button>
</div>
<div className={styles.buttonContainer}>
<Button
to="/add/new"
kind={kinds.PRIMARY}
>
{translate('AddNewMovie')}
</Button>
</div>
</div>
);
}
NoCollection.propTypes = {
totalItems: PropTypes.number.isRequired
};
export default NoCollection;

View File

@@ -1,117 +0,0 @@
$hoverScale: 1.05;
.content {
border-radius: 5px;
transition: all 200ms ease-in;
&:hover {
z-index: 2;
box-shadow: 0 0 10px $black;
transition: all 200ms ease-in;
.poster {
opacity: 0.5;
transition: opacity 100ms linear 100ms;
}
.overlayTitle {
opacity: 1;
transition: opacity 100ms linear 100ms;
}
}
}
.posterContainer {
position: relative;
}
.poster {
position: relative;
display: block;
background-color: $defaultColor;
}
.overlay {
position: absolute;
top: 0;
left: 0;
display: flex;
justify-content: flex-end;
flex-direction: column;
width: 100%;
height: 100%;
}
.overlayTitle {
padding: 5px;
color: $offWhite;
text-align: left;
font-weight: bold;
font-size: 15px;
opacity: 0;
transition: opacity 0;
}
.title {
@add-mixin truncate;
background-color: #fafbfc;
text-align: center;
font-size: $smallFontSize;
}
.controls {
position: absolute;
bottom: 10px;
left: 10px;
z-index: 3;
border-radius: 4px;
background-color: #707070;
color: $white;
font-size: $smallFontSize;
opacity: 0;
transition: opacity 0;
}
.action {
composes: button from '~Components/Link/IconButton.css';
&:hover {
color: $radarrYellow;
}
}
@media only screen and (max-width: $breakpointSmall) {
.container {
padding: 5px;
}
}
.editorSelect {
position: absolute;
top: 10px;
z-index: 3;
}
.externalLinks {
margin-left: 0.5em;
}
.link {
composes: link from '~Components/Link/Link.css';
position: relative;
display: block;
background-color: $defaultColor;
}
.monitorToggleButton {
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 25px;
color: $white;
&:hover {
color: $iconButtonHoverLightColor;
}
}

View File

@@ -1,191 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
import MoviePoster from 'Movie/MoviePoster';
import AddNewCollectionMovieModal from './../AddNewCollectionMovieModal';
import styles from './CollectionMovie.css';
class CollectionMovie extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
hasPosterError: false,
isEditMovieModalOpen: false,
isNewAddMovieModalOpen: false
};
}
//
// Listeners
onEditMoviePress = () => {
this.setState({ isEditMovieModalOpen: true });
};
onEditMovieModalClose = () => {
this.setState({ isEditMovieModalOpen: false });
};
onAddMoviePress = () => {
this.setState({ isNewAddMovieModalOpen: true });
};
onAddMovieModalClose = () => {
this.setState({ isNewAddMovieModalOpen: false });
};
onPosterLoad = () => {
if (this.state.hasPosterError) {
this.setState({ hasPosterError: false });
}
};
onPosterLoadError = () => {
if (!this.state.hasPosterError) {
this.setState({ hasPosterError: true });
}
};
//
// Render
render() {
const {
id,
title,
overview,
year,
tmdbId,
images,
monitored,
hasFile,
folder,
isAvailable,
isExistingMovie,
posterWidth,
posterHeight,
detailedProgressBar,
onMonitorTogglePress,
collectionId
} = this.props;
const {
isEditMovieModalOpen,
isNewAddMovieModalOpen
} = this.state;
const linkProps = id ? { to: `/movie/${tmdbId}` } : { onPress: this.onAddMoviePress };
const elementStyle = {
width: `${posterWidth}px`,
height: `${posterHeight}px`,
borderRadius: '5px'
};
return (
<div className={styles.content}>
<div className={styles.posterContainer}>
{
isExistingMovie &&
<div className={styles.editorSelect}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={20}
onPress={onMonitorTogglePress}
/>
</div>
}
<Link
className={styles.link}
style={elementStyle}
{...linkProps}
>
<MoviePoster
className={styles.poster}
style={elementStyle}
images={images}
size={250}
lazy={false}
overflow={true}
onError={this.onPosterLoadError}
onLoad={this.onPosterLoad}
/>
<div className={styles.overlay}>
<div className={styles.overlayTitle}>
{title}
</div>
{
id &&
<div className={styles.overlayStatus}>
<MovieIndexProgressBar
monitored={monitored}
hasFile={hasFile}
status={status}
bottomRadius={true}
posterWidth={posterWidth}
detailedProgressBar={detailedProgressBar}
isAvailable={isAvailable}
/>
</div>
}
</div>
</Link>
</div>
<AddNewCollectionMovieModal
isOpen={isNewAddMovieModalOpen && !isExistingMovie}
tmdbId={tmdbId}
title={title}
year={year}
overview={overview}
images={images}
folder={folder}
onModalClose={this.onAddMovieModalClose}
collectionId={collectionId}
/>
<EditMovieModalConnector
isOpen={isEditMovieModalOpen}
movieId={id}
onModalClose={this.onEditMovieModalClose}
onDeleteMoviePress={this.onDeleteMoviePress}
/>
</div>
);
}
}
CollectionMovie.propTypes = {
id: PropTypes.number,
title: PropTypes.string.isRequired,
year: PropTypes.number.isRequired,
overview: PropTypes.string.isRequired,
monitored: PropTypes.bool,
collectionId: PropTypes.number.isRequired,
hasFile: PropTypes.bool,
folder: PropTypes.string,
isAvailable: PropTypes.bool,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
posterWidth: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
detailedProgressBar: PropTypes.bool.isRequired,
isExistingMovie: PropTypes.bool,
tmdbId: PropTypes.number.isRequired,
imdbId: PropTypes.string,
youTubeTrailerId: PropTypes.string,
onMonitorTogglePress: PropTypes.func.isRequired
};
export default CollectionMovie;

View File

@@ -1,59 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import CollectionMovie from './CollectionMovie';
function createMapStateToProps() {
return createSelector(
createDimensionsSelector(),
createCollectionExistingMovieSelector(),
(dimensions, existingMovie) => {
return {
isSmallScreen: dimensions.isSmallScreen,
isExistingMovie: !!existingMovie,
...existingMovie
};
}
);
}
const mapDispatchToProps = {
toggleMovieMonitored
};
class CollectionMovieConnector extends Component {
//
// Listeners
onMonitorTogglePress = (monitored) => {
this.props.toggleMovieMonitored({
movieId: this.props.id,
monitored
});
};
//
// Render
render() {
return (
<CollectionMovie
{...this.props}
onMonitorTogglePress={this.onMonitorTogglePress}
/>
);
}
}
CollectionMovieConnector.propTypes = {
id: PropTypes.number,
monitored: PropTypes.bool,
toggleMovieMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieConnector);

View File

@@ -1,50 +0,0 @@
.movie {
display: flex;
align-items: stretch;
overflow: hidden;
margin: 2px 4px;
border: 1px solid $borderColor;
border-radius: 4px;
background-color: #eee;
cursor: default;
}
.movieTitle {
padding: 0 4px;
}
.movieStatus {
padding: 0 4px;
border-left: 4px;
border-left-style: solid;
background-color: $white;
color: $defaultColor;
}
.primary {
border-color: $primaryColor;
}
.danger {
border-color: $dangerColor;
}
.success {
border-color: $successColor;
}
.purple {
border-color: $purple;
}
.warning {
border-color: $warningColor;
}
.info {
border-color: $infoColor;
}
.queue {
border-color: $queueColor;
}

View File

@@ -1,84 +0,0 @@
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import getStatusStyle from 'Utilities/Movie/getStatusStyle';
import translate from 'Utilities/String/translate';
import styles from './CollectionMovieLabel.css';
class CollectionMovieLabel extends Component {
//
// Render
render() {
const {
id,
title,
status,
monitored,
isAvailable,
hasFile,
onMonitorTogglePress,
isSaving
} = this.props;
return (
<div className={styles.movie}>
<div className={styles.movieTitle}>
{
id &&
<MonitorToggleButton
monitored={monitored}
isSaving={isSaving}
onPress={onMonitorTogglePress}
/>
}
<span>
{
title
}
</span>
</div>
{
id &&
<div
className={classNames(
styles.movieStatus,
styles[getStatusStyle(status, monitored, hasFile, isAvailable, 'kinds')]
)}
>
{
hasFile ? translate('Downloaded') : translate('Missing')
}
</div>
}
</div>
);
}
}
CollectionMovieLabel.propTypes = {
id: PropTypes.number,
title: PropTypes.string.isRequired,
status: PropTypes.string,
isAvailable: PropTypes.bool,
monitored: PropTypes.bool,
hasFile: PropTypes.bool,
isSaving: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
movieFileId: PropTypes.number,
onMonitorTogglePress: PropTypes.func.isRequired
};
CollectionMovieLabel.defaultProps = {
isSaving: false,
statistics: {
episodeFileCount: 0,
totalEpisodeCount: 0,
percentOfEpisodes: 0
}
};
export default CollectionMovieLabel;

View File

@@ -1,59 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
import createCollectionExistingMovieSelector from 'Store/Selectors/createCollectionExistingMovieSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import CollectionMovieLabel from './CollectionMovieLabel';
function createMapStateToProps() {
return createSelector(
createDimensionsSelector(),
createCollectionExistingMovieSelector(),
(dimensions, existingMovie) => {
return {
isSmallScreen: dimensions.isSmallScreen,
isExistingMovie: !!existingMovie,
...existingMovie
};
}
);
}
const mapDispatchToProps = {
toggleMovieMonitored
};
class CollectionMovieLabelConnector extends Component {
//
// Listeners
onMonitorTogglePress = (monitored) => {
this.props.toggleMovieMonitored({
movieId: this.props.id,
monitored
});
};
//
// Render
render() {
return (
<CollectionMovieLabel
{...this.props}
onMonitorTogglePress={this.onMonitorTogglePress}
/>
);
}
}
CollectionMovieLabelConnector.propTypes = {
id: PropTypes.number,
monitored: PropTypes.bool,
toggleMovieMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionMovieLabelConnector);

View File

@@ -1,137 +0,0 @@
$hoverScale: 1.05;
.content {
display: flex;
flex-grow: 1;
}
.editorSelect {
position: relative;
top: 0;
left: 5px;
}
.titleRow {
position: relative;
display: flex;
justify-content: space-between;
flex: 0 0 auto;
}
.toggleMonitoredContainer {
align-self: center;
margin-right: 10px;
}
.titleContainer {
display: flex;
margin-bottom: 5px;
}
.sliderContainer {
display: block;
}
.labelsContainer {
display: flex;
flex-wrap: wrap;
}
.moviesContainer {
margin-bottom: 5px;
}
.movie {
padding: 7px;
}
.info {
display: flex;
flex: 1 0 1px;
flex-direction: column;
overflow: hidden;
padding-left: 10px;
}
.title {
@add-mixin truncate;
font-weight: 300;
font-size: 30px;
line-height: 40px;
}
.actions {
white-space: nowrap;
}
.details {
display: flex;
justify-content: space-between;
flex: 1 0 auto;
margin-bottom: 5px;
}
.defaults {
margin-bottom: 5px;
font-weight: 300;
font-size: 20px;
}
.detailsLabel {
composes: label from '~Components/Label.css';
margin: 5px 10px 5px 0;
}
.path,
.status,
.genres,
.qualityProfileName {
margin-left: 8px;
font-weight: 300;
font-size: 15px;
}
.overview {
overflow: hidden;
min-height: 0;
}
.monitorToggleButton {
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 25px;
&:hover {
color: $iconButtonHoverLightColor;
}
}
@media only screen and (max-width: $breakpointSmall) {
.navigationButtons {
display: none;
}
.title {
@add-mixin truncate;
font-weight: 300;
font-size: 22px;
}
.toggleMonitoredContainer {
align-self: center;
margin-right: 5px;
}
.monitorToggleButton {
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 20px;
&:hover {
color: $iconButtonHoverLightColor;
}
}
}

View File

@@ -1,340 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Slider from 'react-slick';
import TextTruncate from 'react-text-truncate';
import EditCollectionModalConnector from 'Collection/Edit/EditCollectionModalConnector';
import CheckInput from 'Components/Form/CheckInput';
import Icon from 'Components/Icon';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import { icons, sizes } from 'Helpers/Props';
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
import dimensions from 'Styles/Variables/dimensions';
import fonts from 'Styles/Variables/fonts';
import translate from 'Utilities/String/translate';
import CollectionMovieConnector from './CollectionMovieConnector';
import CollectionMovieLabelConnector from './CollectionMovieLabelConnector';
import styles from './CollectionOverview.css';
import 'slick-carousel/slick/slick.css';
import 'slick-carousel/slick/slick-theme.css';
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
// Hardcoded height beased on line-height of 32 + bottom margin of 10. 19 + 5 for List Row
// Less side-effecty than using react-measure.
const titleRowHeight = 100;
function getContentHeight(rowHeight, isSmallScreen) {
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
return rowHeight - padding;
}
class CollectionOverview extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isEditCollectionModalOpen: false,
isNewAddMovieModalOpen: false
};
}
//
// Control
setSliderRef = (ref) => {
this.setState({ slider: ref });
};
//
// Listeners
onPress = () => {
this.setState({ isNewAddMovieModalOpen: true });
};
onEditCollectionPress = () => {
this.setState({ isEditCollectionModalOpen: true });
};
onEditCollectionModalClose = () => {
this.setState({ isEditCollectionModalOpen: false });
};
onAddMovieModalClose = () => {
this.setState({ isNewAddMovieModalOpen: false });
};
onChange = ({ value, shiftKey }) => {
const {
id,
onSelectedChange
} = this.props;
onSelectedChange({ id, value, shiftKey });
};
//
// Render
render() {
const {
monitored,
qualityProfileId,
rootFolderPath,
genres,
id,
title,
movies,
overview,
missingMovies,
posterHeight,
posterWidth,
rowHeight,
isSmallScreen,
isSelected,
onMonitorTogglePress
} = this.props;
const {
showDetails,
showOverview,
showPosters,
detailedProgressBar
} = this.props.overviewOptions;
const {
isEditCollectionModalOpen
} = this.state;
const contentHeight = getContentHeight(rowHeight, isSmallScreen);
const overviewHeight = contentHeight - titleRowHeight - posterHeight;
const sliderSettings = {
arrows: false,
dots: false,
infinite: false,
slidesToShow: 1,
slidesToScroll: 1,
variableWidth: true
};
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.editorSelect}>
<CheckInput
className={styles.checkInput}
name={id.toString()}
value={isSelected}
onChange={this.onChange}
/>
</div>
<div className={styles.info} style={{ maxHeight: contentHeight }}>
<div className={styles.titleRow}>
<div className={styles.titleContainer}>
<div className={styles.toggleMonitoredContainer}>
<MonitorToggleButton
className={styles.monitorToggleButton}
monitored={monitored}
size={isSmallScreen ? 20 : 25}
onPress={onMonitorTogglePress}
/>
</div>
<div className={styles.title}>
{title}
</div>
<IconButton
name={icons.EDIT}
title={translate('EditCollection')}
onPress={this.onEditCollectionPress}
/>
</div>
{
showPosters &&
<div className={styles.navigationButtons}>
<IconButton
name={icons.ARROW_LEFT}
title={translate('ScrollMovies')}
onPress={this.state.slider?.slickPrev}
size={20}
/>
<IconButton
name={icons.ARROW_RIGHT}
title={translate('ScrollMovies')}
onPress={this.state.slider?.slickNext}
size={20}
/>
</div>
}
</div>
{
showDetails &&
<div className={styles.defaults}>
<Label
className={styles.detailsLabel}
size={sizes.MEDIUM}
>
<Icon
name={icons.DRIVE}
size={13}
/>
<span className={styles.status}>
{`${missingMovies} missing movie(s)`}
</span>
</Label>
{
!isSmallScreen &&
<Label
className={styles.detailsLabel}
size={sizes.MEDIUM}
>
<Icon
name={icons.PROFILE}
size={13}
/>
<span className={styles.qualityProfileName}>
{
<QualityProfileNameConnector
qualityProfileId={qualityProfileId}
/>
}
</span>
</Label>
}
{
!isSmallScreen &&
<Label
className={styles.detailsLabel}
size={sizes.MEDIUM}
>
<Icon
name={icons.FOLDER}
size={13}
/>
<span className={styles.path}>
{rootFolderPath}
</span>
</Label>
}
{
!isSmallScreen &&
<Label
className={styles.detailsLabel}
size={sizes.MEDIUM}
>
<Icon
name={icons.PROFILE}
size={13}
/>
<span className={styles.genres}>
{genres.join(', ')}
</span>
</Label>
}
</div>
}
{
showOverview &&
<div className={styles.details}>
<div className={styles.overview}>
<TextTruncate
line={Math.floor(overviewHeight / (defaultFontSize * lineHeight))}
text={overview}
/>
</div>
</div>
}
{
showPosters ?
<div className={styles.sliderContainer}>
<Slider ref={this.setSliderRef} {...sliderSettings}>
{movies.map((movie) => (
<div className={styles.movie} key={movie.tmdbId}>
<CollectionMovieConnector
key={movie.tmdbId}
posterWidth={posterWidth}
posterHeight={posterHeight}
detailedProgressBar={detailedProgressBar}
collectionId={id}
{...movie}
/>
</div>
))}
</Slider>
</div> :
<div className={styles.labelsContainer}>
{movies.map((movie) => (
<CollectionMovieLabelConnector
key={movie.tmdbId}
collectionId={id}
{...movie}
/>
))}
</div>
}
</div>
</div>
<EditCollectionModalConnector
isOpen={isEditCollectionModalOpen}
collectionId={id}
onModalClose={this.onEditCollectionModalClose}
/>
</div>
);
}
}
CollectionOverview.propTypes = {
id: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
qualityProfileId: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired,
searchOnAdd: PropTypes.bool.isRequired,
rootFolderPath: PropTypes.string.isRequired,
tmdbId: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
overview: PropTypes.string.isRequired,
movies: PropTypes.arrayOf(PropTypes.object).isRequired,
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
missingMovies: PropTypes.number.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
rowHeight: PropTypes.number.isRequired,
posterHeight: PropTypes.number.isRequired,
posterWidth: PropTypes.number.isRequired,
overviewOptions: PropTypes.object.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onMonitorTogglePress: PropTypes.func.isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default CollectionOverview;

View File

@@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { toggleCollectionMonitored } from 'Store/Actions/movieCollectionActions';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import CollectionOverview from './CollectionOverview';
function createMapStateToProps() {
return createSelector(
createDimensionsSelector(),
(dimensions) => {
return {
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
const mapDispatchToProps = {
toggleCollectionMonitored
};
class CollectionOverviewConnector extends Component {
//
// Listeners
onMonitorTogglePress = (monitored) => {
this.props.toggleCollectionMonitored({
collectionId: this.props.collectionId,
monitored
});
};
//
// Render
render() {
return (
<CollectionOverview
{...this.props}
onMonitorTogglePress={this.onMonitorTogglePress}
/>
);
}
}
CollectionOverviewConnector.propTypes = {
collectionId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
toggleCollectionMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(CollectionOverviewConnector);

View File

@@ -1,15 +0,0 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: $tableRowHoverBackgroundColor;
}
}
}
.externalLinks {
margin-right: 0.5em;
}

View File

@@ -1,272 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Grid, WindowScroller } from 'react-virtualized';
import CollectionItemConnector from 'Collection/CollectionItemConnector';
import Measure from 'Components/Measure';
import dimensions from 'Styles/Variables/dimensions';
import getIndexOfFirstCharacter from 'Utilities/Array/getIndexOfFirstCharacter';
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
import CollectionOverviewConnector from './CollectionOverviewConnector';
import styles from './CollectionOverviews.css';
// Poster container dimensions
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
function calculatePosterWidth(posterSize, isSmallScreen) {
const maxiumPosterWidth = isSmallScreen ? 152 : 162;
if (posterSize === 'large') {
return maxiumPosterWidth;
}
if (posterSize === 'medium') {
return Math.floor(maxiumPosterWidth * 0.75);
}
return Math.floor(maxiumPosterWidth * 0.5);
}
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
const heights = [
overviewOptions.showPosters ? posterHeight : 75,
isSmallScreen ? columnPaddingSmallScreen : columnPadding
];
return heights.reduce((acc, height) => acc + height + 80, 0);
}
function calculatePosterHeight(posterWidth) {
return Math.ceil((250 / 170) * posterWidth);
}
class CollectionOverviews extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
width: 0,
columnCount: 1,
posterWidth: 162,
posterHeight: 238,
rowHeight: calculateRowHeight(238, null, props.isSmallScreen, {})
};
this._grid = null;
}
componentDidUpdate(prevProps, prevState) {
const {
items,
sortKey,
overviewOptions,
jumpToCharacter,
scrollTop,
isSmallScreen
} = this.props;
const {
width,
rowHeight,
scrollRestored
} = this.state;
if (prevProps.sortKey !== sortKey ||
prevProps.overviewOptions !== overviewOptions) {
this.calculateGrid(this.state.width, isSmallScreen);
}
if (
this._grid &&
(prevState.width !== width ||
prevState.rowHeight !== rowHeight ||
hasDifferentItemsOrOrder(prevProps.items, items) ||
prevProps.overviewOptions !== overviewOptions)) {
// recomputeGridSize also forces Grid to discard its cache of rendered cells
this._grid.recomputeGridSize();
}
if (this._grid && scrollTop !== 0 && !scrollRestored) {
this.setState({ scrollRestored: true });
this._grid.scrollToPosition({ scrollTop });
}
if (jumpToCharacter != null && jumpToCharacter !== prevProps.jumpToCharacter) {
const index = getIndexOfFirstCharacter(items, jumpToCharacter);
if (this._grid && index != null) {
this._grid.scrollToCell({
rowIndex: index,
columnIndex: 0
});
}
}
}
//
// Control
setGridRef = (ref) => {
this._grid = ref;
};
calculateGrid = (width = this.state.width, isSmallScreen) => {
const {
sortKey,
overviewOptions
} = this.props;
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
const posterHeight = calculatePosterHeight(posterWidth);
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
this.setState({
width,
posterWidth,
posterHeight,
rowHeight
});
};
cellRenderer = ({ key, rowIndex, style }) => {
const {
items,
sortKey,
overviewOptions,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
isSmallScreen,
selectedState,
onSelectedChange
} = this.props;
const {
posterWidth,
posterHeight,
rowHeight
} = this.state;
const collection = items[rowIndex];
if (!collection) {
return null;
}
return (
<div
className={styles.container}
key={key}
style={style}
>
<CollectionItemConnector
key={collection.id}
component={CollectionOverviewConnector}
sortKey={sortKey}
posterWidth={posterWidth}
posterHeight={posterHeight}
rowHeight={rowHeight}
overviewOptions={overviewOptions}
showRelativeDates={showRelativeDates}
shortDateFormat={shortDateFormat}
longDateFormat={longDateFormat}
timeFormat={timeFormat}
isSmallScreen={isSmallScreen}
collectionId={collection.id}
isSelected={selectedState[collection.id]}
onSelectedChange={onSelectedChange}
/>
</div>
);
};
//
// Listeners
onMeasure = ({ width }) => {
this.calculateGrid(width, this.props.isSmallScreen);
};
//
// Render
render() {
const {
isSmallScreen,
scroller,
items,
selectedState
} = this.props;
const {
width,
rowHeight
} = this.state;
return (
<Measure
whitelist={['width']}
onMeasure={this.onMeasure}
>
<WindowScroller
scrollElement={isSmallScreen ? undefined : scroller}
>
{({ height, registerChild, onChildScroll, scrollTop }) => {
if (!height) {
return <div />;
}
return (
<div ref={registerChild}>
<Grid
ref={this.setGridRef}
className={styles.grid}
autoHeight={true}
height={height}
columnCount={1}
columnWidth={width}
rowCount={items.length}
rowHeight={rowHeight}
width={width}
onScroll={onChildScroll}
scrollTop={scrollTop}
overscanRowCount={2}
cellRenderer={this.cellRenderer}
selectedState={selectedState}
scrollToAlignment={'start'}
isScrollingOptout={true}
/>
</div>
);
}
}
</WindowScroller>
</Measure>
);
}
}
CollectionOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
overviewOptions: PropTypes.object.isRequired,
jumpToCharacter: PropTypes.string,
scrollTop: PropTypes.number.isRequired,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired,
selectedState: PropTypes.object.isRequired,
onSelectedChange: PropTypes.func.isRequired
};
export default CollectionOverviews;

View File

@@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import CollectionOverviews from './CollectionOverviews';
function createMapStateToProps() {
return createSelector(
(state) => state.movieCollections.overviewOptions,
createUISettingsSelector(),
createDimensionsSelector(),
(overviewOptions, uiSettings, dimensions) => {
return {
overviewOptions,
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat,
isSmallScreen: dimensions.isSmallScreen
};
}
);
}
export default connect(createMapStateToProps)(CollectionOverviews);

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