1
0
mirror of https://github.com/Radarr/Radarr.git synced 2026-04-18 21:35:51 -04:00

Compare commits

..

1 Commits

Author SHA1 Message Date
ta264 b92e2c6601 FFMpeg 5.0.1 2022-04-10 12:28:11 -05:00
1445 changed files with 12697 additions and 54555 deletions
+8 -7
View File
@@ -46,6 +46,7 @@ csharp_style_var_elsewhere = true:suggestion
# Stylecop Rules # Stylecop Rules
dotnet_diagnostic.SA0001.severity = none dotnet_diagnostic.SA0001.severity = none
dotnet_diagnostic.SA1005.severity = none
dotnet_diagnostic.SA1025.severity = none dotnet_diagnostic.SA1025.severity = none
dotnet_diagnostic.SA1101.severity = none dotnet_diagnostic.SA1101.severity = none
dotnet_diagnostic.SA1116.severity = none dotnet_diagnostic.SA1116.severity = none
@@ -68,7 +69,6 @@ dotnet_diagnostic.SA1406.severity = suggestion
dotnet_diagnostic.SA1410.severity = suggestion dotnet_diagnostic.SA1410.severity = suggestion
dotnet_diagnostic.SA1411.severity = suggestion dotnet_diagnostic.SA1411.severity = suggestion
dotnet_diagnostic.SA1413.severity = none dotnet_diagnostic.SA1413.severity = none
dotnet_diagnostic.SA1512.severity = none
dotnet_diagnostic.SA1516.severity = none dotnet_diagnostic.SA1516.severity = none
dotnet_diagnostic.SA1600.severity = none dotnet_diagnostic.SA1600.severity = none
dotnet_diagnostic.SA1601.severity = none dotnet_diagnostic.SA1601.severity = none
@@ -167,7 +167,6 @@ dotnet_diagnostic.CA1309.severity = suggestion
dotnet_diagnostic.CA1310.severity = suggestion dotnet_diagnostic.CA1310.severity = suggestion
dotnet_diagnostic.CA1401.severity = suggestion dotnet_diagnostic.CA1401.severity = suggestion
dotnet_diagnostic.CA1416.severity = suggestion dotnet_diagnostic.CA1416.severity = suggestion
dotnet_diagnostic.CA1419.severity = suggestion
dotnet_diagnostic.CA1507.severity = suggestion dotnet_diagnostic.CA1507.severity = suggestion
dotnet_diagnostic.CA1508.severity = suggestion dotnet_diagnostic.CA1508.severity = suggestion
dotnet_diagnostic.CA1707.severity = suggestion dotnet_diagnostic.CA1707.severity = suggestion
@@ -183,6 +182,9 @@ dotnet_diagnostic.CA1720.severity = suggestion
dotnet_diagnostic.CA1721.severity = suggestion dotnet_diagnostic.CA1721.severity = suggestion
dotnet_diagnostic.CA1724.severity = suggestion dotnet_diagnostic.CA1724.severity = suggestion
dotnet_diagnostic.CA1725.severity = suggestion dotnet_diagnostic.CA1725.severity = suggestion
dotnet_diagnostic.CA1801.severity = suggestion
dotnet_diagnostic.CA1802.severity = suggestion
dotnet_diagnostic.CA1805.severity = suggestion
dotnet_diagnostic.CA1806.severity = suggestion dotnet_diagnostic.CA1806.severity = suggestion
dotnet_diagnostic.CA1810.severity = suggestion dotnet_diagnostic.CA1810.severity = suggestion
dotnet_diagnostic.CA1812.severity = suggestion dotnet_diagnostic.CA1812.severity = suggestion
@@ -194,11 +196,13 @@ dotnet_diagnostic.CA1819.severity = suggestion
dotnet_diagnostic.CA1822.severity = suggestion dotnet_diagnostic.CA1822.severity = suggestion
dotnet_diagnostic.CA1823.severity = suggestion dotnet_diagnostic.CA1823.severity = suggestion
dotnet_diagnostic.CA1824.severity = suggestion dotnet_diagnostic.CA1824.severity = suggestion
dotnet_diagnostic.CA1848.severity = suggestion
dotnet_diagnostic.CA2000.severity = suggestion dotnet_diagnostic.CA2000.severity = suggestion
dotnet_diagnostic.CA2002.severity = suggestion dotnet_diagnostic.CA2002.severity = suggestion
dotnet_diagnostic.CA2007.severity = suggestion dotnet_diagnostic.CA2007.severity = suggestion
dotnet_diagnostic.CA2008.severity = suggestion dotnet_diagnostic.CA2008.severity = suggestion
dotnet_diagnostic.CA2009.severity = suggestion
dotnet_diagnostic.CA2010.severity = suggestion
dotnet_diagnostic.CA2011.severity = suggestion
dotnet_diagnostic.CA2012.severity = suggestion dotnet_diagnostic.CA2012.severity = suggestion
dotnet_diagnostic.CA2013.severity = suggestion dotnet_diagnostic.CA2013.severity = suggestion
dotnet_diagnostic.CA2100.severity = suggestion dotnet_diagnostic.CA2100.severity = suggestion
@@ -229,9 +233,6 @@ dotnet_diagnostic.CA2243.severity = suggestion
dotnet_diagnostic.CA2244.severity = suggestion dotnet_diagnostic.CA2244.severity = suggestion
dotnet_diagnostic.CA2245.severity = suggestion dotnet_diagnostic.CA2245.severity = suggestion
dotnet_diagnostic.CA2246.severity = suggestion dotnet_diagnostic.CA2246.severity = suggestion
dotnet_diagnostic.CA2249.severity = suggestion
dotnet_diagnostic.CA2251.severity = suggestion
dotnet_diagnostic.CA2254.severity = suggestion
dotnet_diagnostic.CA3061.severity = suggestion dotnet_diagnostic.CA3061.severity = suggestion
dotnet_diagnostic.CA3075.severity = suggestion dotnet_diagnostic.CA3075.severity = suggestion
dotnet_diagnostic.CA3076.severity = suggestion dotnet_diagnostic.CA3076.severity = suggestion
@@ -259,7 +260,7 @@ dotnet_diagnostic.CA5392.severity = suggestion
dotnet_diagnostic.CA5394.severity = suggestion dotnet_diagnostic.CA5394.severity = suggestion
dotnet_diagnostic.CA5397.severity = suggestion dotnet_diagnostic.CA5397.severity = suggestion
dotnet_diagnostic.SYSLIB0006.severity = none
[*.{js,html,js,hbs,less,css}] [*.{js,html,js,hbs,less,css}]
charset = utf-8 charset = utf-8
+10
View File
@@ -0,0 +1,10 @@
{
"paths": [
"frontend/src/**/*.js",
"src/NzbDrone.Core/Localization/Core/*.json"
],
"ignored": [
"**/node_modules/**/*"
],
"port": 5004
}
+1 -1
View File
@@ -1,6 +1,6 @@
# These are supported funding model platforms # These are supported funding model platforms
github: radarr github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username patreon: # Replace with a single Patreon username
open_collective: radarr open_collective: radarr
ko_fi: # Replace with a single Ko-fi username ko_fi: # Replace with a single Ko-fi username
+6 -8
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? label: Is there an existing issue for this?
description: Please search to see if an open or closed issue already exists for the bug you encountered. If a bug exists and is closed note that it may only be fixed in an unstable branch. description: Please search to see if an issue already exists for the bug you encountered.
options: options:
- label: I have searched the existing open and closed issues - label: I have searched the existing issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
@@ -42,14 +42,12 @@ body:
- **Docker Install**: Yes - **Docker Install**: Yes
- **Using Reverse Proxy**: No - **Using Reverse Proxy**: No
- **Browser**: Firefox 90 (If UI related) - **Browser**: Firefox 90 (If UI related)
- **Database**: Sqlite 3.36.0
value: | value: |
- OS: - OS:
- Radarr: - Radarr:
- Docker Install: - Docker Install:
- Using Reverse Proxy: - Using Reverse Proxy:
- Browser: - Browser:
- Database:
render: markdown render: markdown
validations: validations:
required: true required: true
+2 -2
View File
@@ -5,9 +5,9 @@ body:
- type: checkboxes - type: checkboxes
attributes: attributes:
label: Is there an existing issue for this? label: Is there an existing issue for this?
description: Please search to see if an open or closed issue already exists for the feature you are requesting. If a request exists and is closed note that it may only be fixed in an unstable branch. description: Please search to see if an issue already exists for the feature you are requesting.
options: options:
- label: I have searched the existing open and closed issues - label: I have searched the existing issues
required: true required: true
- type: textarea - type: textarea
attributes: attributes:
+41
View File
@@ -0,0 +1,41 @@
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
-5
View File
@@ -5,13 +5,8 @@ on:
schedule: schedule:
- cron: '0 0 * * *' - cron: '0 0 * * *'
permissions: {}
jobs: jobs:
lock: lock:
permissions:
issues: write # to lock issues (dessant/lock-threads)
pull-requests: write # to lock PRs (dessant/lock-threads)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@v2 - uses: dessant/lock-threads@v2
-4
View File
@@ -4,12 +4,8 @@ on:
issues: issues:
types: [labeled, unlabeled, reopened] types: [labeled, unlabeled, reopened]
permissions: {}
jobs: jobs:
support: support:
permissions:
issues: write # to modify issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/support-requests@v2 - uses: dessant/support-requests@v2
+21 -2
View File
@@ -166,8 +166,27 @@ packages.config.md5sum
# Common IntelliJ Platform excludes # Common IntelliJ Platform excludes
# Ignore Rider projects completely for now # User specific
.idea/ **/.idea/**/workspace.xml
**/.idea/**/tasks.xml
**/.idea/shelf/*
**/.idea/dictionaries
**/.idea/.idea.Radarr.Posix
**/.idea/.idea.Radarr.Windows
# Sensitive or high-churn files
**/.idea/**/dataSources/
**/.idea/**/dataSources.ids
**/.idea/**/dataSources.xml
**/.idea/**/dataSources.local.xml
**/.idea/**/sqlDataSources.xml
**/.idea/**/dynamic.xml
# Rider
# Rider auto-generates .iml files, and contentModel.xml
**/.idea/**/*.iml
**/.idea/**/contentModel.xml
**/.idea/**/modules.xml
# ignore node_modules symlink # ignore node_modules symlink
node_modules node_modules
-132
View File
@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<development@radarr.video>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
-9
View File
@@ -76,15 +76,6 @@ Thank you to [<img src="/Logo/jetbrains.svg" alt="JetBrains" width="32"> JetBrai
* [<img src="/Logo/rider.svg" alt="Rider" width="32"> Rider](http://www.jetbrains.com/rider/) * [<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/) * [<img src="/Logo/dottrace.svg" alt="dotTrace" width="32"> dotTrace](http://www.jetbrains.com/dottrace/)
## DigitalOcean
This project is also supported by DigitalOcean
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>
### License ### License
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) * [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
-8
View File
@@ -1,8 +0,0 @@
# Security Policy
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities on Discord (preferred) to
any of the Servarr Dev role holders (red names) or via email: development@servarr.com. You will receive a response from
us within 72 hours. If the issue is confirmed, we will release a patch as soon
as possible depending on complexity/severity.
+52 -205
View File
@@ -9,13 +9,13 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '5.0.0' majorVersion: '4.1.0'
minorVersion: $[counter('minorVersion', 2000)] minorVersion: $[counter('minorVersion', 2000)]
radarrVersion: '$(majorVersion).$(minorVersion)' radarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(radarrVersion)' buildName: '$(Build.SourceBranchName).$(radarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.400' dotnetVersion: '6.0.201'
nodeVersion: '16.X' nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
@@ -27,7 +27,6 @@ trigger:
include: include:
- develop - develop
- master - master
- zeus
pr: pr:
branches: branches:
@@ -98,14 +97,15 @@ stages:
- bash: | - bash: |
BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props BUNDLEDVERSIONS=${AGENT_TOOLSDIRECTORY}/dotnet/sdk/${DOTNETVERSION}/Microsoft.NETCoreSdk.BundledVersions.props
echo $BUNDLEDVERSIONS echo $BUNDLEDVERSIONS
grep osx-x64 $BUNDLEDVERSIONS
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then if grep -q freebsd-x64 $BUNDLEDVERSIONS; then
echo "Extra platforms already enabled" echo "BSD already enabled"
else else
echo "Enabling extra platform support" echo "Enabling BSD support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64/' $BUNDLEDVERSIONS
fi fi
displayName: Enable Extra Platform Support displayName: Enable FreeBSD Support
- bash: ./build.sh --backend --enable-extra-platforms - bash: ./build.sh --backend --enable-bsd
displayName: Build Radarr Backend displayName: Build Radarr Backend
- bash: | - bash: |
find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \; find ${OUTPUTFOLDER} -type f ! -path "*/publish/*" -exec rm -rf {} \;
@@ -119,28 +119,24 @@ stages:
displayName: Publish Backend displayName: Publish Backend
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/win-x64/publish' - publish: '$(testsFolder)/net6.0/win-x64/publish'
artifact: win-x64-tests artifact: WindowsCoreTests
displayName: Publish win-x64 Test Package displayName: Publish Windows Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-x64/publish' - publish: '$(testsFolder)/net6.0/linux-x64/publish'
artifact: linux-x64-tests artifact: LinuxCoreTests
displayName: Publish linux-x64 Test Package displayName: Publish Linux 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
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/linux-musl-x64/publish' - publish: '$(testsFolder)/net6.0/linux-musl-x64/publish'
artifact: linux-musl-x64-tests artifact: LinuxMuslCoreTests
displayName: Publish linux-musl-x64 Test Package displayName: Publish Linux Musl Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/freebsd-x64/publish' - publish: '$(testsFolder)/net6.0/freebsd-x64/publish'
artifact: freebsd-x64-tests artifact: FreebsdCoreTests
displayName: Publish freebsd-x64 Test Package displayName: Publish FreeBSD Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- publish: '$(testsFolder)/net6.0/osx-x64/publish' - publish: '$(testsFolder)/net6.0/osx-x64/publish'
artifact: osx-x64-tests artifact: MacCoreTests
displayName: Publish osx-x64 Test Package displayName: Publish MacOS Test Package
condition: and(succeeded(), eq(variables['osName'], 'Windows')) condition: and(succeeded(), eq(variables['osName'], 'Windows'))
- stage: Build_Frontend - stage: Build_Frontend
@@ -174,6 +170,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --frontend - bash: ./build.sh --frontend
@@ -243,7 +240,7 @@ stages:
artifactName: WindowsFrontend artifactName: WindowsFrontend
targetPath: _output targetPath: _output
displayName: Fetch Frontend displayName: Fetch Frontend
- bash: ./build.sh --packages --enable-extra-platforms - bash: ./build.sh --packages --enable-bsd
displayName: Create Packages displayName: Create Packages
- bash: | - bash: |
find . -name "ffprobe" -exec chmod a+x {} \; find . -name "ffprobe" -exec chmod a+x {} \;
@@ -251,28 +248,28 @@ stages:
find . -name "Radarr.Update" -exec chmod a+x {} \; find . -name "Radarr.Update" -exec chmod a+x {} \;
displayName: Set executable bits displayName: Set executable bits
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create win-x64 zip displayName: Create Windows Core zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create win-x86 zip displayName: Create Windows x86 Core zip
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).windows-core-x86.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0 rootFolderOrFile: $(artifactsFolder)/win-x86/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-x64 app displayName: Create MacOS x64 Core app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-x64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-x64 tar displayName: Create MacOS x64 Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -280,14 +277,14 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-arm64 app displayName: Create MacOS arm64 Core app
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-app-core-arm64.zip'
archiveType: 'zip' archiveType: 'zip'
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64-app/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create osx-arm64 tar displayName: Create MacOS arm64 Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).osx-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -295,7 +292,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/osx-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-x64 tar displayName: Create Linux Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -303,7 +300,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-x64 tar displayName: Create Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -311,15 +308,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-x64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-x86 tar displayName: Create ARM32 Linux Core 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
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -327,7 +316,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-arm tar displayName: Create ARM32 Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -335,7 +324,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-arm64 tar displayName: Create ARM64 Linux Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -343,7 +332,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create linux-musl-arm64 tar displayName: Create ARM64 Linux Musl Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).linux-musl-core-arm64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -351,7 +340,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create freebsd-x64 tar displayName: Create FreeBSD Core Core tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Radarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -418,22 +407,22 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'osx-x64' testName: 'MacCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'win-x64' testName: 'WindowsCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'linux-x64' testName: 'LinuxCore'
poolName: 'Azure Pipelines' poolName: 'Azure Pipelines'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
FreebsdCore: FreebsdCore:
osName: 'Linux' osName: 'Linux'
testName: 'freebsd-x64' testName: 'FreebsdCore'
poolName: 'FreeBSD' poolName: 'FreeBSD'
imageName: imageName:
@@ -452,7 +441,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)-tests' artifactName: '$(testName)Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- powershell: Set-Service SCardSvr -StartupType Manual - powershell: Set-Service SCardSvr -StartupType Manual
displayName: Enable Windows Test Service displayName: Enable Windows Test Service
@@ -486,12 +475,8 @@ stages:
matrix: matrix:
alpine: alpine:
testName: 'Musl Net Core' testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
linux-x86:
testName: 'linux-x86'
artifactName: linux-x86-tests
containerImage: ghcr.io/servarr/testimages:linux-x86
pool: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -502,15 +487,9 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -536,62 +515,6 @@ stages:
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
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: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(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 \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests'
failTaskOnFailedTests: true
- stage: Integration - stage: Integration
displayName: Integration displayName: Integration
@@ -619,17 +542,17 @@ stages:
matrix: matrix:
MacCore: MacCore:
osName: 'Mac' osName: 'Mac'
testName: 'osx-x64' testName: 'MacCore'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Radarr.*.osx-core-x64.tar.gz' pattern: 'Radarr.*.osx-core-x64.tar.gz'
WindowsCore: WindowsCore:
osName: 'Windows' osName: 'Windows'
testName: 'win-x64' testName: 'WindowsCore'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Radarr.*.windows-core-x64.zip' pattern: 'Radarr.*.windows-core-x64.zip'
LinuxCore: LinuxCore:
osName: 'Linux' osName: 'Linux'
testName: 'linux-x64' testName: 'LinuxCore'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Radarr.*.linux-core-x64.tar.gz' pattern: 'Radarr.*.linux-core-x64.tar.gz'
@@ -646,7 +569,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(testName)-tests' artifactName: '$(testName)Tests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -676,68 +599,6 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
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: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres14 \
-e POSTGRES_PASSWORD=radarr \
-e POSTGRES_USER=radarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_FreeBSD - job: Integration_FreeBSD
displayName: Integration Native FreeBSD displayName: Integration Native FreeBSD
dependsOn: Prepare dependsOn: Prepare
@@ -755,14 +616,14 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: 'freebsd-x64-tests' artifactName: 'FreebsdCoreTests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: Packages artifactName: Packages
itemPattern: '**/$(pattern)' itemPattern: '/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory) targetPath: $(Build.ArtifactStagingDirectory)
- bash: | - bash: |
mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin mkdir -p ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin
@@ -791,15 +652,10 @@ stages:
strategy: strategy:
matrix: matrix:
alpine: alpine:
testName: 'linux-musl-x64' testName: 'Musl Net Core'
artifactName: linux-musl-x64-tests artifactName: LinuxMuslCoreTests
containerImage: ghcr.io/servarr/testimages:alpine containerImage: ghcr.io/servarr/testimages:alpine
pattern: 'Radarr.*.linux-musl-core-x64.tar.gz' 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: pool:
vmImage: ${{ variables.linuxImage }} vmImage: ${{ variables.linuxImage }}
@@ -809,15 +665,9 @@ stages:
steps: steps:
- task: UseDotNet@2 - task: UseDotNet@2
displayName: 'Install .NET' displayName: 'Install .net core'
inputs: inputs:
version: $(dotnetVersion) version: $(dotnetVersion)
condition: and(succeeded(), ne(variables['testName'], 'linux-x86'))
- bash: |
SDKURL=$(curl -s https://api.github.com/repos/Servarr/dotnet-linux-x86/releases | jq -rc '.[].assets[].browser_download_url' | grep sdk-${DOTNETVERSION}.*gz$)
curl -fsSL $SDKURL | tar xzf - -C /opt/dotnet
displayName: 'Install .NET'
condition: and(succeeded(), eq(variables['testName'], 'linux-x86'))
- checkout: none - checkout: none
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Test Artifact displayName: Download Test Artifact
@@ -863,19 +713,16 @@ stages:
matrix: matrix:
Linux: Linux:
osName: 'Linux' osName: 'Linux'
artifactName: 'linux-x64'
imageName: ${{ variables.linuxImage }} imageName: ${{ variables.linuxImage }}
pattern: 'Radarr.*.linux-core-x64.tar.gz' pattern: 'Radarr.*.linux-core-x64.tar.gz'
failBuild: true failBuild: true
Mac: Mac:
osName: 'Mac' osName: 'Mac'
artifactName: 'osx-x64'
imageName: ${{ variables.macImage }} imageName: ${{ variables.macImage }}
pattern: 'Radarr.*.osx-core-x64.tar.gz' pattern: 'Radarr.*.osx-core-x64.tar.gz'
failBuild: true failBuild: true
Windows: Windows:
osName: 'Windows' osName: 'Windows'
artifactName: 'win-x64'
imageName: ${{ variables.windowsImage }} imageName: ${{ variables.windowsImage }}
pattern: 'Radarr.*.windows-core-x64.zip' pattern: 'Radarr.*.windows-core-x64.zip'
failBuild: true failBuild: true
@@ -893,7 +740,7 @@ stages:
displayName: Download Test Artifact displayName: Download Test Artifact
inputs: inputs:
buildType: 'current' buildType: 'current'
artifactName: '$(artifactName)-tests' artifactName: '$(osName)CoreTests'
targetPath: $(testsFolder) targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2 - task: DownloadPipelineArtifact@2
displayName: Download Build Artifact displayName: Download Build Artifact
@@ -978,6 +825,7 @@ stages:
key: 'yarn | "$(osName)" | yarn.lock' key: 'yarn | "$(osName)" | yarn.lock'
restoreKeys: | restoreKeys: |
yarn | "$(osName)" yarn | "$(osName)"
yarn
path: $(yarnCacheFolder) path: $(yarnCacheFolder)
displayName: Cache Yarn packages displayName: Cache Yarn packages
- bash: ./build.sh --lint - bash: ./build.sh --lint
@@ -1093,7 +941,7 @@ stages:
projectVersion: '$(radarrVersion)' projectVersion: '$(radarrVersion)'
extraProperties: | extraProperties: |
sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/** sonar.exclusions=**/obj/**,**/*.dll,**/NzbDrone.Core.Test/Files/**/*,./frontend/**,**/ExternalModules/**,./src/Libraries/**
sonar.coverage.exclusions=**/Radarr.Api.V*/**/* sonar.coverage.exclusions=**/Radarr.Api.V3/**/*
sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml sonar.cs.opencover.reportsPaths=$(Build.SourcesDirectory)/CoverageResults/**/coverage.opencover.xml
sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml sonar.cs.nunit.reportsPaths=$(Build.SourcesDirectory)/TestResult.xml
- bash: | - bash: |
@@ -1144,5 +992,4 @@ stages:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) SYSTEM_ACCESSTOKEN: $(System.AccessToken)
DISCORDCHANNELID: $(discordChannelId) DISCORDCHANNELID: $(discordChannelId)
DISCORDWEBHOOKKEY: $(discordWebhookKey) DISCORDWEBHOOKKEY: $(discordWebhookKey)
DISCORDTHREADID: $(discordThreadId)
+12 -32
View File
@@ -25,22 +25,14 @@ UpdateVersionNumber()
fi fi
} }
EnableExtraPlatformsInSDK() EnableBsdSupport()
{ {
SDK_PATH=$(dotnet --list-sdks | grep -P '6\.\d\.\d+' | head -1 | sed 's/\(6\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g') #todo enable sdk with
BUNDLEDVERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props" #SDK_PATH=$(dotnet --list-sdks | grep -P '5\.\d\.\d+' | head -1 | sed 's/\(5\.[0-9]*\.[0-9]*\).*\[\(.*\)\]/\2\/\1/g')
if grep -q freebsd-x64 $BUNDLEDVERSIONS; then # BUNDLED_VERSIONS="${SDK_PATH}/Microsoft.NETCoreSdk.BundledVersions.props"
echo "Extra platforms already enabled"
else
echo "Enabling extra platform support"
sed -i.ORI 's/osx-x64/osx-x64;freebsd-x64;linux-x86/' $BUNDLEDVERSIONS
fi
}
EnableExtraPlatforms()
{
if grep -qv freebsd-x64 src/Directory.Build.props; then if grep -qv freebsd-x64 src/Directory.Build.props; then
sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64;linux-x86</RuntimeIdentifiers>^g" src/Directory.Build.props sed -i'' -e "s^<RuntimeIdentifiers>\(.*\)</RuntimeIdentifiers>^<RuntimeIdentifiers>\1;freebsd-x64</RuntimeIdentifiers>^g" src/Directory.Build.props
fi fi
} }
@@ -300,8 +292,7 @@ if [ $# -eq 0 ]; then
PACKAGES=YES PACKAGES=YES
INSTALLER=NO INSTALLER=NO
LINT=YES LINT=YES
ENABLE_EXTRA_PLATFORMS=NO ENABLE_BSD=NO
ENABLE_EXTRA_PLATFORMS_IN_SDK=NO
fi fi
while [[ $# -gt 0 ]] while [[ $# -gt 0 ]]
@@ -313,12 +304,8 @@ case $key in
BACKEND=YES BACKEND=YES
shift # past argument shift # past argument
;; ;;
--enable-bsd|--enable-extra-platforms) --enable-bsd)
ENABLE_EXTRA_PLATFORMS=YES ENABLE_BSD=YES
shift # past argument
;;
--enable-extra-platforms-in-sdk)
ENABLE_EXTRA_PLATFORMS_IN_SDK=YES
shift # past argument shift # past argument
;; ;;
-r|--runtime) -r|--runtime)
@@ -362,17 +349,12 @@ esac
done done
set -- "${POSITIONAL[@]}" # restore positional parameters set -- "${POSITIONAL[@]}" # restore positional parameters
if [ "$ENABLE_EXTRA_PLATFORMS_IN_SDK" = "YES" ];
then
EnableExtraPlatformsInSDK
fi
if [ "$BACKEND" = "YES" ]; if [ "$BACKEND" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
EnableExtraPlatforms EnableBsdSupport
fi fi
Build Build
if [[ -z "$RID" || -z "$FRAMEWORK" ]]; if [[ -z "$RID" || -z "$FRAMEWORK" ]];
@@ -382,10 +364,9 @@ then
PackageTests "net6.0" "linux-x64" PackageTests "net6.0" "linux-x64"
PackageTests "net6.0" "linux-musl-x64" PackageTests "net6.0" "linux-musl-x64"
PackageTests "net6.0" "osx-x64" PackageTests "net6.0" "osx-x64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
PackageTests "net6.0" "freebsd-x64" PackageTests "net6.0" "freebsd-x64"
PackageTests "net6.0" "linux-x86"
fi fi
else else
PackageTests "$FRAMEWORK" "$RID" PackageTests "$FRAMEWORK" "$RID"
@@ -424,10 +405,9 @@ then
Package "net6.0" "linux-musl-arm" Package "net6.0" "linux-musl-arm"
Package "net6.0" "osx-x64" Package "net6.0" "osx-x64"
Package "net6.0" "osx-arm64" Package "net6.0" "osx-arm64"
if [ "$ENABLE_EXTRA_PLATFORMS" = "YES" ]; if [ "$ENABLE_BSD" = "YES" ];
then then
Package "net6.0" "freebsd-x64" Package "net6.0" "freebsd-x64"
Package "net6.0" "linux-x86"
fi fi
else else
Package "$FRAMEWORK" "$RID" Package "$FRAMEWORK" "$RID"
+1 -1
View File
@@ -29,7 +29,7 @@ dotnet msbuild -restore $slnFile -p:Configuration=Debug -p:Platform=$platform -p
dotnet new tool-manifest dotnet new tool-manifest
dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli dotnet tool install --version 6.3.0 Swashbuckle.AspNetCore.Cli
dotnet tool run swagger tofile --output ./src/Radarr.Api.V4/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v4 & dotnet tool run swagger tofile --output ./src/Radarr.Api.V3/openapi.json "$outputFolder/net6.0/$RUNTIME/radarr.console.dll" v3 &
sleep 45 sleep 45
+1 -4
View File
@@ -39,7 +39,6 @@ module.exports = {
plugins: [ plugins: [
'filenames', 'filenames',
'react', 'react',
'react-hooks',
'simple-import-sort', 'simple-import-sort',
'import' 'import'
], ],
@@ -309,9 +308,7 @@ module.exports = {
'react/react-in-jsx-scope': 2, 'react/react-in-jsx-scope': 2,
'react/self-closing-comp': 2, 'react/self-closing-comp': 2,
'react/sort-comp': 2, 'react/sort-comp': 2,
'react/jsx-wrap-multilines': 2, 'react/jsx-wrap-multilines': 2
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error'
}, },
overrides: [ overrides: [
{ {
+1
View File
@@ -223,6 +223,7 @@ module.exports = (env) => {
{ {
loader: 'url-loader', loader: 'url-loader',
options: { options: {
limit: 10240,
mimetype: 'application/font-woff', mimetype: 'application/font-woff',
emitFile: false, emitFile: false,
name: 'Content/Fonts/[name].[ext]' name: 'Content/Fonts/[name].[ext]'
@@ -1,4 +1,3 @@
// eslint-disable-next-line filenames/match-exported
const loaderUtils = require('loader-utils'); const loaderUtils = require('loader-utils');
module.exports = function cssVariablesLoader(source) { module.exports = function cssVariablesLoader(source) {
+2 -1
View File
@@ -1,6 +1,7 @@
const reload = require('require-nocache')(module); const reload = require('require-nocache')(module);
const cssVarsFiles = [ const cssVarsFiles = [
'./src/Styles/Variables/colors',
'./src/Styles/Variables/dimensions', './src/Styles/Variables/dimensions',
'./src/Styles/Variables/fonts', './src/Styles/Variables/fonts',
'./src/Styles/Variables/animations', './src/Styles/Variables/animations',
@@ -28,4 +29,4 @@ module.exports = {
'postcss-color-function', 'postcss-color-function',
'postcss-nested' 'postcss-nested'
] ]
}; };
@@ -10,12 +10,6 @@
width: 80px; width: 80px;
} }
.customFormatScore {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 55px;
}
.releaseGroup { .releaseGroup {
composes: cell from '~Components/Table/Cells/TableRowCell.css'; composes: cell from '~Components/Table/Cells/TableRowCell.css';
+1 -25
View File
@@ -9,7 +9,6 @@ import MovieFormats from 'Movie/MovieFormats';
import MovieLanguage from 'Movie/MovieLanguage'; import MovieLanguage from 'Movie/MovieLanguage';
import MovieQuality from 'Movie/MovieQuality'; import MovieQuality from 'Movie/MovieQuality';
import MovieTitleLink from 'Movie/MovieTitleLink'; import MovieTitleLink from 'Movie/MovieTitleLink';
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
import HistoryDetailsModal from './Details/HistoryDetailsModal'; import HistoryDetailsModal from './Details/HistoryDetailsModal';
import HistoryEventTypeCell from './HistoryEventTypeCell'; import HistoryEventTypeCell from './HistoryEventTypeCell';
import styles from './HistoryRow.css'; import styles from './HistoryRow.css';
@@ -56,7 +55,6 @@ class HistoryRow extends Component {
movie, movie,
quality, quality,
customFormats, customFormats,
customFormatScore,
languages, languages,
qualityCutoffNotMet, qualityCutoffNotMet,
eventType, eventType,
@@ -170,17 +168,6 @@ class HistoryRow extends Component {
); );
} }
if (name === 'customFormatScore') {
return (
<TableRowCell
key={name}
className={styles.customFormatScore}
>
{formatCustomFormatScore(customFormatScore)}
</TableRowCell>
);
}
if (name === 'releaseGroup') { if (name === 'releaseGroup') {
return ( return (
<TableRowCell <TableRowCell
@@ -192,16 +179,6 @@ class HistoryRow extends Component {
); );
} }
if (name === 'sourceTitle') {
return (
<TableRowCell
key={name}
>
{sourceTitle}
</TableRowCell>
);
}
if (name === 'details') { if (name === 'details') {
return ( return (
<TableRowCell <TableRowCell
@@ -242,9 +219,8 @@ HistoryRow.propTypes = {
movie: PropTypes.object.isRequired, movie: PropTypes.object.isRequired,
languages: PropTypes.arrayOf(PropTypes.object).isRequired, languages: PropTypes.arrayOf(PropTypes.object).isRequired,
quality: PropTypes.object.isRequired, quality: PropTypes.object.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object),
customFormatScore: PropTypes.number.isRequired,
qualityCutoffNotMet: PropTypes.bool.isRequired, qualityCutoffNotMet: PropTypes.bool.isRequired,
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
sourceTitle: PropTypes.string.isRequired, sourceTitle: PropTypes.string.isRequired,
date: PropTypes.string.isRequired, date: PropTypes.string.isRequired,
@@ -1,13 +1,13 @@
.torrent { .torrent {
composes: label from '~Components/Label.css'; composes: label from '~Components/Label.css';
border-color: var(--torrentColor); border-color: $torrentColor;
background-color: var(--torrentColor); background-color: $torrentColor;
} }
.usenet { .usenet {
composes: label from '~Components/Label.css'; composes: label from '~Components/Label.css';
border-color: var(--usenetColor); border-color: $usenetColor;
background-color: var(--usenetColor); background-color: $usenetColor;
} }
+9 -23
View File
@@ -75,23 +75,13 @@ class Queue extends Component {
return; return;
} }
const nextState = {};
if (prevProps.items !== items) {
nextState.items = items;
}
const selectedIds = this.getSelectedIds(); const selectedIds = this.getSelectedIds();
const isPendingSelected = _.some(this.props.items, (item) => { const isPendingSelected = _.some(this.props.items, (item) => {
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay'; return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
}); });
if (isPendingSelected !== this.state.isPendingSelected) { if (isPendingSelected !== this.state.isPendingSelected) {
nextState.isPendingSelected = isPendingSelected; this.setState({ isPendingSelected });
}
if (!_.isEmpty(nextState)) {
this.setState(nextState);
} }
} }
@@ -224,29 +214,26 @@ class Queue extends Component {
<PageContentBody> <PageContentBody>
{ {
isRefreshing && !isAllPopulated ? isRefreshing && !isAllPopulated &&
<LoadingIndicator /> : <LoadingIndicator />
null
} }
{ {
!isRefreshing && hasError ? !isRefreshing && hasError &&
<div> <div>
{translate('FailedToLoadQueue')} {translate('FailedToLoadQueue')}
</div> : </div>
null
} }
{ {
isAllPopulated && !hasError && !items.length ? isAllPopulated && !hasError && !items.length &&
<div> <div>
{translate('QueueIsEmpty')} {translate('QueueIsEmpty')}
</div> : </div>
null
} }
{ {
isAllPopulated && !hasError && !!items.length ? isAllPopulated && !hasError && !!items.length &&
<div> <div>
<Table <Table
columns={columns} columns={columns}
@@ -281,8 +268,7 @@ class Queue extends Component {
isFetching={isRefreshing} isFetching={isRefreshing}
{...otherProps} {...otherProps}
/> />
</div> : </div>
null
} }
</PageContentBody> </PageContentBody>
-12
View File
@@ -128,7 +128,6 @@ class QueueRow extends Component {
{ {
columns.map((column) => { columns.map((column) => {
const { const {
name, name,
isVisible isVisible
@@ -235,16 +234,6 @@ class QueueRow extends Component {
); );
} }
if (name === 'year') {
return (
<TableRowCell key={name}>
{
movie ? movie.year : ''
}
</TableRowCell>
);
}
if (name === 'title') { if (name === 'title') {
return ( return (
<TableRowCell key={name}> <TableRowCell key={name}>
@@ -373,7 +362,6 @@ QueueRow.propTypes = {
estimatedCompletionTime: PropTypes.string, estimatedCompletionTime: PropTypes.string,
timeleft: PropTypes.string, timeleft: PropTypes.string,
size: PropTypes.number, size: PropTypes.number,
year: PropTypes.number,
sizeleft: PropTypes.number, sizeleft: PropTypes.number,
showRelativeDates: PropTypes.bool.isRequired, showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
@@ -6,12 +6,12 @@
.searchIconContainer { .searchIconContainer {
width: 58px; width: 58px;
height: 46px; height: 46px;
border: 1px solid var(--inputBorderColor); border: 1px solid $inputBorderColor;
border-right: none; border-right: none;
border-radius: 4px; border-radius: 4px;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
background-color: var(--searchIconContainerBackgroundColor); background-color: #edf1f2;
text-align: center; text-align: center;
line-height: 46px; line-height: 46px;
} }
@@ -25,7 +25,7 @@
} }
.clearLookupButton { .clearLookupButton {
border: 1px solid var(--inputBorderColor); border: 1px solid $inputBorderColor;
border-left: none; border-left: none;
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
@@ -4,7 +4,7 @@
.year { .year {
margin-left: 5px; margin-left: 5px;
color: var(--disabledColor); color: $disabledColor;
} }
.poster { .poster {
@@ -20,6 +20,10 @@ class AddNewMovieModalContent extends Component {
// //
// Listeners // Listeners
onQualityProfileIdChange = ({ value }) => {
this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) });
};
onAddMoviePress = () => { onAddMoviePress = () => {
this.props.onAddMoviePress(); this.props.onAddMoviePress();
}; };
@@ -36,7 +40,7 @@ class AddNewMovieModalContent extends Component {
isAdding, isAdding,
rootFolderPath, rootFolderPath,
monitor, monitor,
qualityProfileIds, qualityProfileId,
minimumAvailability, minimumAvailability,
searchForMovie, searchForMovie,
folder, folder,
@@ -126,9 +130,9 @@ class AddNewMovieModalContent extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds" name="qualityProfileId"
onChange={onInputChange} onChange={this.onQualityProfileIdChange}
{...qualityProfileIds} {...qualityProfileId}
/> />
</FormGroup> </FormGroup>
@@ -185,7 +189,7 @@ AddNewMovieModalContent.propTypes = {
addError: PropTypes.object, addError: PropTypes.object,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object), qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired, searchForMovie: PropTypes.object.isRequired,
folder: PropTypes.string.isRequired, folder: PropTypes.string.isRequired,
@@ -58,7 +58,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId, tmdbId,
rootFolderPath, rootFolderPath,
monitor, monitor,
qualityProfileIds, qualityProfileId,
minimumAvailability, minimumAvailability,
searchForMovie, searchForMovie,
tags tags
@@ -68,7 +68,7 @@ class AddNewMovieModalContentConnector extends Component {
tmdbId, tmdbId,
rootFolderPath: rootFolderPath.value, rootFolderPath: rootFolderPath.value,
monitor: monitor.value, monitor: monitor.value,
qualityProfileIds: qualityProfileIds.value, qualityProfileId: qualityProfileId.value,
minimumAvailability: minimumAvailability.value, minimumAvailability: minimumAvailability.value,
searchForMovie: searchForMovie.value, searchForMovie: searchForMovie.value,
tags: tags.value tags: tags.value
@@ -93,7 +93,7 @@ AddNewMovieModalContentConnector.propTypes = {
tmdbId: PropTypes.number.isRequired, tmdbId: PropTypes.number.isRequired,
rootFolderPath: PropTypes.object, rootFolderPath: PropTypes.object,
monitor: PropTypes.object.isRequired, monitor: PropTypes.object.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.object), qualityProfileId: PropTypes.object,
minimumAvailability: PropTypes.object.isRequired, minimumAvailability: PropTypes.object.isRequired,
searchForMovie: PropTypes.object.isRequired, searchForMovie: PropTypes.object.isRequired,
tags: PropTypes.object.isRequired, tags: PropTypes.object.isRequired,
@@ -9,15 +9,13 @@
.underlay { .underlay {
@add-mixin cover; @add-mixin cover;
background-color: var(--addMovieBackgroundColor); background-color: $white;
transition: background 500ms; transition: background 500ms;
&:hover { &:hover {
background-color: var(--pageBackground); background-color: #eaf2ff;
box-shadow: 0 0 12px var(--black);
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
transition: all 200ms ease-in;
} }
} }
@@ -33,7 +31,7 @@
display: block; display: block;
margin-right: 20px; margin-right: 20px;
height: 250px; height: 250px;
background-color: var(--defaultColor); background-color: $defaultColor;
} }
.content { .content {
@@ -58,7 +56,7 @@
.year { .year {
margin-left: 10px; margin-left: 10px;
color: var(--disabledColor); color: $disabledColor;
} }
.icons { .icons {
@@ -77,7 +75,7 @@
.exclusionIcon { .exclusionIcon {
margin-left: 10px; margin-left: 10px;
color: var(--dangerColor); color: $dangerColor;
pointer-events: all; pointer-events: all;
} }
@@ -1,9 +1,9 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Label from 'Components/Label'; import Label from 'Components/Label';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import TmdbRating from 'Components/TmdbRating';
import Tooltip from 'Components/Tooltip/Tooltip'; import Tooltip from 'Components/Tooltip/Tooltip';
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props'; import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks'; import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
@@ -72,19 +72,15 @@ class AddNewMovieSearchResult extends Component {
colorImpairedMode, colorImpairedMode,
id, id,
monitored, monitored,
hasFile,
isAvailable, isAvailable,
queueStatus, queueStatus,
queueState, queueState,
runtime, runtime,
movieRuntimeFormat, movieRuntimeFormat,
certification, certification
statistics
} = this.props; } = this.props;
const {
movieFileCount
} = statistics;
const { const {
isNewAddMovieModalOpen isNewAddMovieModalOpen
} = this.state; } = this.state;
@@ -124,7 +120,7 @@ class AddNewMovieSearchResult extends Component {
isExistingMovie && isExistingMovie &&
<MovieIndexProgressBar <MovieIndexProgressBar
monitored={monitored} monitored={monitored}
hasFile={movieFileCount > 0} hasFile={hasFile}
status={status} status={status}
posterWidth={posterWidth} posterWidth={posterWidth}
detailedProgressBar={true} detailedProgressBar={true}
@@ -194,7 +190,7 @@ class AddNewMovieSearchResult extends Component {
<div> <div>
<Label size={sizes.LARGE}> <Label size={sizes.LARGE}>
<TmdbRating <HeartRating
ratings={ratings} ratings={ratings}
iconSize={13} iconSize={13}
/> />
@@ -237,7 +233,7 @@ class AddNewMovieSearchResult extends Component {
{ {
isExistingMovie && isSmallScreen && isExistingMovie && isSmallScreen &&
<MovieStatusLabel <MovieStatusLabel
hasMovieFiles={movieFileCount > 0} hasMovieFiles={hasFile}
monitored={monitored} monitored={monitored}
isAvailable={isAvailable} isAvailable={isAvailable}
id={id} id={id}
@@ -294,14 +290,7 @@ AddNewMovieSearchResult.propTypes = {
queueState: PropTypes.string, queueState: PropTypes.string,
runtime: PropTypes.number.isRequired, runtime: PropTypes.number.isRequired,
movieRuntimeFormat: PropTypes.string.isRequired, movieRuntimeFormat: PropTypes.string.isRequired,
certification: PropTypes.string, certification: PropTypes.string
statistics: PropTypes.object
};
AddNewMovieSearchResult.defaultProps = {
statistics: {
movieFileCount: 0
}
}; };
export default AddNewMovieSearchResult; export default AddNewMovieSearchResult;
@@ -25,13 +25,13 @@ class ImportMovieFooter extends Component {
const { const {
defaultMonitor, defaultMonitor,
defaultQualityProfileIds, defaultQualityProfileId,
defaultMinimumAvailability defaultMinimumAvailability
} = props; } = props;
this.state = { this.state = {
monitor: defaultMonitor, monitor: defaultMonitor,
qualityProfileIds: defaultQualityProfileIds, qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability minimumAvailability: defaultMinimumAvailability
}; };
} }
@@ -39,16 +39,16 @@ class ImportMovieFooter extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { const {
defaultMonitor, defaultMonitor,
defaultQualityProfileIds, defaultQualityProfileId,
defaultMinimumAvailability, defaultMinimumAvailability,
isMonitorMixed, isMonitorMixed,
isQualityProfileIdsMixed, isQualityProfileIdMixed,
isMinimumAvailabilityMixed isMinimumAvailabilityMixed
} = this.props; } = this.props;
const { const {
monitor, monitor,
qualityProfileIds, qualityProfileId,
minimumAvailability minimumAvailability
} = this.state; } = this.state;
@@ -60,10 +60,10 @@ class ImportMovieFooter extends Component {
newState.monitor = defaultMonitor; newState.monitor = defaultMonitor;
} }
if (isQualityProfileIdsMixed && qualityProfileIds !== MIXED) { if (isQualityProfileIdMixed && qualityProfileId !== MIXED) {
newState.qualityProfileIds = MIXED; newState.qualityProfileId = MIXED;
} else if (!isQualityProfileIdsMixed && qualityProfileIds !== defaultQualityProfileIds) { } else if (!isQualityProfileIdMixed && qualityProfileId !== defaultQualityProfileId) {
newState.qualityProfileIds = defaultQualityProfileIds; newState.qualityProfileId = defaultQualityProfileId;
} }
if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) { if (isMinimumAvailabilityMixed && minimumAvailability !== MIXED) {
@@ -94,7 +94,7 @@ class ImportMovieFooter extends Component {
isImporting, isImporting,
isLookingUpMovie, isLookingUpMovie,
isMonitorMixed, isMonitorMixed,
isQualityProfileIdsMixed, isQualityProfileIdMixed,
isMinimumAvailabilityMixed, isMinimumAvailabilityMixed,
hasUnsearchedItems, hasUnsearchedItems,
importError, importError,
@@ -105,7 +105,7 @@ class ImportMovieFooter extends Component {
const { const {
monitor, monitor,
qualityProfileIds, qualityProfileId,
minimumAvailability minimumAvailability
} = this.state; } = this.state;
@@ -148,10 +148,10 @@ class ImportMovieFooter extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds" name="qualityProfileId"
value={qualityProfileIds} value={qualityProfileId}
isDisabled={!selectedCount} isDisabled={!selectedCount}
includeMixed={isQualityProfileIdsMixed} includeMixed={isQualityProfileIdMixed}
onChange={this.onInputChange} onChange={this.onInputChange}
/> />
</div> </div>
@@ -225,19 +225,13 @@ class ImportMovieFooter extends Component {
body={ body={
<ul> <ul>
{ {
Array.isArray(importError.responseJSON) ? importError.responseJSON.map((error, index) => {
importError.responseJSON.map((error, index) => { return (
return ( <li key={index}>
<li key={index}> {error.errorMessage}
{error.errorMessage} </li>
</li> );
); })
}) :
<li>
{
JSON.stringify(importError.responseJSON)
}
</li>
} }
</ul> </ul>
} }
@@ -257,10 +251,10 @@ ImportMovieFooter.propTypes = {
isImporting: PropTypes.bool.isRequired, isImporting: PropTypes.bool.isRequired,
isLookingUpMovie: PropTypes.bool.isRequired, isLookingUpMovie: PropTypes.bool.isRequired,
defaultMonitor: PropTypes.string.isRequired, defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number), defaultQualityProfileId: PropTypes.number,
defaultMinimumAvailability: PropTypes.string, defaultMinimumAvailability: PropTypes.string,
isMonitorMixed: PropTypes.bool.isRequired, isMonitorMixed: PropTypes.bool.isRequired,
isQualityProfileIdsMixed: PropTypes.bool.isRequired, isQualityProfileIdMixed: PropTypes.bool.isRequired,
isMinimumAvailabilityMixed: PropTypes.bool.isRequired, isMinimumAvailabilityMixed: PropTypes.bool.isRequired,
hasUnsearchedItems: PropTypes.bool.isRequired, hasUnsearchedItems: PropTypes.bool.isRequired,
importError: PropTypes.object, importError: PropTypes.object,
@@ -18,7 +18,7 @@ function createMapStateToProps() {
(addMovie, importMovie, selectedIds) => { (addMovie, importMovie, selectedIds) => {
const { const {
monitor: defaultMonitor, monitor: defaultMonitor,
qualityProfileIds: defaultQualityProfileIds, qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability minimumAvailability: defaultMinimumAvailability
} = addMovie.defaults; } = addMovie.defaults;
@@ -30,7 +30,7 @@ function createMapStateToProps() {
} = importMovie; } = importMovie;
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor'); const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
const isQualityProfileIdsMixed = isMixed(items, selectedIds, defaultQualityProfileIds, 'qualityProfileIds'); const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability'); const isMinimumAvailabilityMixed = isMixed(items, selectedIds, defaultMinimumAvailability, 'minimumAvailability');
const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated); const hasUnsearchedItems = !isLookingUpMovie && items.some((item) => !item.isPopulated);
@@ -39,10 +39,10 @@ function createMapStateToProps() {
isLookingUpMovie, isLookingUpMovie,
isImporting, isImporting,
defaultMonitor, defaultMonitor,
defaultQualityProfileIds, defaultQualityProfileId,
defaultMinimumAvailability, defaultMinimumAvailability,
isMonitorMixed, isMonitorMixed,
isQualityProfileIdsMixed, isQualityProfileIdMixed,
isMinimumAvailabilityMixed, isMinimumAvailabilityMixed,
importError, importError,
hasUnsearchedItems hasUnsearchedItems
@@ -11,7 +11,7 @@ function ImportMovieRow(props) {
const { const {
id, id,
monitor, monitor,
qualityProfileIds, qualityProfileId,
minimumAvailability, minimumAvailability,
selectedMovie, selectedMovie,
isExistingMovie, isExistingMovie,
@@ -62,8 +62,8 @@ function ImportMovieRow(props) {
<VirtualTableRowCell className={styles.qualityProfile}> <VirtualTableRowCell className={styles.qualityProfile}>
<FormInputGroup <FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT} type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds" name="qualityProfileId"
value={qualityProfileIds} value={qualityProfileId}
onChange={onInputChange} onChange={onInputChange}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
@@ -74,7 +74,7 @@ function ImportMovieRow(props) {
ImportMovieRow.propTypes = { ImportMovieRow.propTypes = {
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
monitor: PropTypes.string.isRequired, monitor: PropTypes.string.isRequired,
qualityProfileIds: PropTypes.arrayOf(PropTypes.number).isRequired, qualityProfileId: PropTypes.number.isRequired,
minimumAvailability: PropTypes.string.isRequired, minimumAvailability: PropTypes.string.isRequired,
selectedMovie: PropTypes.object, selectedMovie: PropTypes.object,
isExistingMovie: PropTypes.bool.isRequired, isExistingMovie: PropTypes.bool.isRequired,
@@ -15,7 +15,7 @@ class ImportMovieTable extends Component {
const { const {
unmappedFolders, unmappedFolders,
defaultMonitor, defaultMonitor,
defaultQualityProfileIds, defaultQualityProfileId,
defaultMinimumAvailability, defaultMinimumAvailability,
onMovieLookup, onMovieLookup,
onSetImportMovieValue onSetImportMovieValue
@@ -23,7 +23,7 @@ class ImportMovieTable extends Component {
const values = { const values = {
monitor: defaultMonitor, monitor: defaultMonitor,
qualityProfileIds: defaultQualityProfileIds, qualityProfileId: defaultQualityProfileId,
minimumAvailability: defaultMinimumAvailability minimumAvailability: defaultMinimumAvailability
}; };
@@ -167,7 +167,7 @@ ImportMovieTable.propTypes = {
items: PropTypes.arrayOf(PropTypes.object), items: PropTypes.arrayOf(PropTypes.object),
unmappedFolders: PropTypes.arrayOf(PropTypes.object), unmappedFolders: PropTypes.arrayOf(PropTypes.object),
defaultMonitor: PropTypes.string.isRequired, defaultMonitor: PropTypes.string.isRequired,
defaultQualityProfileIds: PropTypes.arrayOf(PropTypes.number), defaultQualityProfileId: PropTypes.number,
defaultMinimumAvailability: PropTypes.string, defaultMinimumAvailability: PropTypes.string,
allSelected: PropTypes.bool.isRequired, allSelected: PropTypes.bool.isRequired,
allUnselected: PropTypes.bool.isRequired, allUnselected: PropTypes.bool.isRequired,
@@ -13,7 +13,7 @@ function createMapStateToProps() {
(addMovie, importMovie, dimensions, allMovies) => { (addMovie, importMovie, dimensions, allMovies) => {
return { return {
defaultMonitor: addMovie.defaults.monitor, defaultMonitor: addMovie.defaults.monitor,
defaultQualityProfileIds: addMovie.defaults.qualityProfileIds, defaultQualityProfileId: addMovie.defaults.qualityProfileId,
defaultMinimumAvailability: addMovie.defaults.minimumAvailability, defaultMinimumAvailability: addMovie.defaults.minimumAvailability,
items: importMovie.items, items: importMovie.items,
isSmallScreen: dimensions.isSmallScreen, isSmallScreen: dimensions.isSmallScreen,
@@ -1,25 +1,8 @@
.container { .movie {
display: flex;
padding: 10px 20px; padding: 10px 20px;
width: 100%; width: 100%;
&:hover { &:hover {
background-color: var(--menuItemHoverBackgroundColor); background-color: $menuItemHoverBackgroundColor;
} }
} }
.movie {
flex: 1 0 0;
overflow: hidden;
}
.tmdbLink {
composes: link from '~Components/Link/Link.css';
margin-left: auto;
color: var(--textColor);
}
.tmdbLinkIcon {
margin-left: 10px;
}
@@ -1,8 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import { icons } from 'Helpers/Props';
import ImportMovieTitle from './ImportMovieTitle'; import ImportMovieTitle from './ImportMovieTitle';
import styles from './ImportMovieSearchResult.css'; import styles from './ImportMovieSearchResult.css';
@@ -20,7 +18,6 @@ class ImportMovieSearchResult extends Component {
render() { render() {
const { const {
tmdbId,
title, title,
year, year,
studio, studio,
@@ -28,30 +25,17 @@ class ImportMovieSearchResult extends Component {
} = this.props; } = this.props;
return ( return (
<div className={styles.container}> <Link
<Link className={styles.movie}
className={styles.movie} onPress={this.onPress}
onPress={this.onPress} >
> <ImportMovieTitle
<ImportMovieTitle title={title}
title={title} year={year}
year={year} network={studio}
network={studio} isExistingMovie={isExistingMovie}
isExistingMovie={isExistingMovie} />
/> </Link>
</Link>
<Link
className={styles.tmdbLink}
to={`https://www.themoviedb.org/movie/${tmdbId}`}
>
<Icon
className={styles.tmdbLinkIcon}
name={icons.EXTERNAL_LINK}
size={16}
/>
</Link>
</div>
); );
} }
} }
@@ -7,10 +7,10 @@
padding: 6px 16px; padding: 6px 16px;
width: 100%; width: 100%;
height: 35px; height: 35px;
border: 1px solid var(--inputBorderColor); border: 1px solid $inputBorderColor;
border-radius: 4px; border-radius: 4px;
background-color: var(--inputBackgroundColor); background-color: $white;
box-shadow: inset 0 1px 1px var(--inputBoxShadowColor); box-shadow: inset 0 1px 1px $inputBoxShadowColor;
} }
.loading { .loading {
@@ -38,9 +38,9 @@
.content { .content {
padding: 4px; padding: 4px;
border: 1px solid var(--inputBorderColor); border: 1px solid $inputBorderColor;
border-radius: 4px; border-radius: 4px;
background-color: var(--inputBackgroundColor); background-color: $white;
} }
.searchContainer { .searchContainer {
@@ -49,12 +49,12 @@
.searchIconContainer { .searchIconContainer {
width: 58px; width: 58px;
border: 1px solid var(--inputBorderColor); border: 1px solid $inputBorderColor;
border-right: none; border-right: none;
border-radius: 4px; border-radius: 4px;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
background-color: var(--searchIconContainerBackgroundColor); background-color: #edf1f2;
text-align: center; text-align: center;
line-height: 33px; line-height: 33px;
} }
@@ -5,7 +5,6 @@ import FormInputButton from 'Components/Form/FormInputButton';
import TextInput from 'Components/Form/TextInput'; import TextInput from 'Components/Form/TextInput';
import Icon from 'Components/Icon'; import Icon from 'Components/Icon';
import Link from 'Components/Link/Link'; import Link from 'Components/Link/Link';
import SpinnerButton from 'Components/Link/SpinnerButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Portal from 'Components/Portal'; import Portal from 'Components/Portal';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
@@ -243,7 +242,7 @@ class ImportMovieSelectMovie extends Component {
<FormInputButton <FormInputButton
kind={kinds.DEFAULT} kind={kinds.DEFAULT}
spinnerIcon={icons.REFRESH} spinnerIcon={icons.REFRESH}
ButtonComponent={SpinnerButton} canSpin={true}
isSpinning={isFetching} isSpinning={isFetching}
onPress={this.onRefreshPress} onPress={this.onRefreshPress}
> >
@@ -9,7 +9,7 @@
.year { .year {
margin-left: 5px; margin-left: 5px;
color: var(--disabledColor); color: $disabledColor;
} }
.existing { .existing {
@@ -152,19 +152,13 @@ class ImportMovieSelectFolder extends Component {
<ul> <ul>
{ {
Array.isArray(saveError.responseJSON) ? saveError.responseJSON.map((e, index) => {
saveError.responseJSON.map((e, index) => { return (
return ( <li key={index}>
<li key={index}> {e.errorMessage}
{e.errorMessage} </li>
</li> );
); })
}) :
<li>
{
JSON.stringify(saveError.responseJSON)
}
</li>
} }
</ul> </ul>
</Alert> : </Alert> :
+4 -7
View File
@@ -4,19 +4,16 @@ import React from 'react';
import DocumentTitle from 'react-document-title'; import DocumentTitle from 'react-document-title';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import PageConnector from 'Components/Page/PageConnector'; import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
function App({ store, history }) { function App({ store, history }) {
return ( return (
<DocumentTitle title={window.Radarr.instanceName}> <DocumentTitle title="Radarr">
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ApplyTheme> <PageConnector>
<PageConnector> <AppRoutes app={App} />
<AppRoutes app={App} /> </PageConnector>
</PageConnector>
</ApplyTheme>
</ConnectedRouter> </ConnectedRouter>
</Provider> </Provider>
</DocumentTitle> </DocumentTitle>
+2 -8
View File
@@ -7,7 +7,6 @@ import QueueConnector from 'Activity/Queue/QueueConnector';
import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector'; import AddNewMovieConnector from 'AddMovie/AddNewMovie/AddNewMovieConnector';
import ImportMovies from 'AddMovie/ImportMovie/ImportMovies'; import ImportMovies from 'AddMovie/ImportMovie/ImportMovies';
import CalendarPageConnector from 'Calendar/CalendarPageConnector'; import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import CollectionConnector from 'Collection/CollectionConnector';
import NotFound from 'Components/NotFound'; import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch'; import Switch from 'Components/Router/Switch';
import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector'; import DiscoverMovieConnector from 'DiscoverMovie/DiscoverMovieConnector';
@@ -22,7 +21,7 @@ import MediaManagementConnector from 'Settings/MediaManagement/MediaManagementCo
import MetadataSettings from 'Settings/Metadata/MetadataSettings'; import MetadataSettings from 'Settings/Metadata/MetadataSettings';
import NotificationSettings from 'Settings/Notifications/NotificationSettings'; import NotificationSettings from 'Settings/Notifications/NotificationSettings';
import Profiles from 'Settings/Profiles/Profiles'; import Profiles from 'Settings/Profiles/Profiles';
import QualityConnector from 'Settings/Quality/QualityConnector'; import Quality from 'Settings/Quality/Quality';
import Settings from 'Settings/Settings'; import Settings from 'Settings/Settings';
import TagSettings from 'Settings/Tags/TagSettings'; import TagSettings from 'Settings/Tags/TagSettings';
import UISettingsConnector from 'Settings/UI/UISettingsConnector'; import UISettingsConnector from 'Settings/UI/UISettingsConnector';
@@ -73,11 +72,6 @@ function AppRoutes(props) {
component={AddNewMovieConnector} component={AddNewMovieConnector}
/> />
<Route
path="/collections"
component={CollectionConnector}
/>
<Route <Route
path="/add/import" path="/add/import"
component={ImportMovies} component={ImportMovies}
@@ -143,7 +137,7 @@ function AppRoutes(props) {
<Route <Route
path="/settings/quality" path="/settings/quality"
component={QualityConnector} component={Quality}
/> />
<Route <Route
-49
View File
@@ -1,49 +0,0 @@
import PropTypes from 'prop-types';
import React, { Fragment, useCallback, useEffect } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import themes from 'Styles/Themes';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.ui.item.theme || window.Radarr.theme,
(
theme
) => {
return {
theme
};
}
);
}
function ApplyTheme({ theme, children }) {
// Update the CSS Variables
const updateCSSVariables = useCallback(() => {
const arrayOfVariableKeys = Object.keys(themes[theme]);
const arrayOfVariableValues = Object.values(themes[theme]);
// Loop through each array key and set the CSS Variables
arrayOfVariableKeys.forEach((cssVariableKey, index) => {
// Based on our snippet from MDN
document.documentElement.style.setProperty(
`--${cssVariableKey}`,
arrayOfVariableValues[index]
);
});
}, [theme]);
// On Component Mount and Component Update
useEffect(() => {
updateCSSVariables(theme);
}, [updateCSSVariables, theme]);
return <Fragment>{children}</Fragment>;
}
ApplyTheme.propTypes = {
theme: PropTypes.string.isRequired,
children: PropTypes.object.isRequired
};
export default connect(createMapStateToProps)(ApplyTheme);
+2 -2
View File
@@ -2,11 +2,11 @@
display: flex; display: flex;
overflow-x: hidden; overflow-x: hidden;
padding: 5px; padding: 5px;
border-bottom: 1px solid var(--borderColor); border-bottom: 1px solid $borderColor;
font-size: $defaultFontSize; font-size: $defaultFontSize;
&:hover { &:hover {
background-color: var(--tableRowHoverBackgroundColor); background-color: $tableRowHoverBackgroundColor;
} }
} }
@@ -24,7 +24,7 @@ function createMissingMovieIdsSelector() {
const inCinemas = movie.inCinemas; const inCinemas = movie.inCinemas;
if ( if (
(!movie.statistics || movie.statistics.movieFileCount === 0) && !movie.hasFile &&
moment(inCinemas).isAfter(start) && moment(inCinemas).isAfter(start) &&
moment(inCinemas).isBefore(end) && moment(inCinemas).isBefore(end) &&
isBefore(movie.inCinemas) && isBefore(movie.inCinemas) &&
+5 -5
View File
@@ -2,8 +2,8 @@
flex: 1 0 14.28%; flex: 1 0 14.28%;
overflow: hidden; overflow: hidden;
min-height: 70px; min-height: 70px;
border-bottom: 1px solid var(--calendarBorderColor); border-bottom: 1px solid $calendarBorderColor;
border-left: 1px solid var(--calendarBorderColor); border-left: 1px solid $calendarBorderColor;
} }
.isSingleDay { .isSingleDay {
@@ -12,14 +12,14 @@
.dayOfMonth { .dayOfMonth {
padding-right: 5px; padding-right: 5px;
border-bottom: 1px solid var(--calendarBorderColor); border-bottom: 1px solid $calendarBorderColor;
text-align: right; text-align: right;
} }
.isToday { .isToday {
background-color: var(--calendarTodayBackgroundColor); background-color: $calendarTodayBackgroundColor;
} }
.isDifferentMonth { .isDifferentMonth {
color: var(--disabledColor); color: $disabledColor;
} }
+1 -1
View File
@@ -1,6 +1,6 @@
.days { .days {
display: flex; display: flex;
border-right: 1px solid var(--calendarBorderColor); border-right: 1px solid $calendarBorderColor;
} }
.day, .day,
+2 -2
View File
@@ -1,6 +1,6 @@
.dayOfWeek { .dayOfWeek {
flex: 1 0 14.28%; flex: 1 0 14.28%;
background-color: var(--calendarBackgroudColor); background-color: #e4eaec;
text-align: center; text-align: center;
} }
@@ -9,5 +9,5 @@
} }
.isToday { .isToday {
background-color: var(--calendarTodayBackgroundColor); background-color: $calendarTodayBackgroundColor;
} }
+16 -66
View File
@@ -1,11 +1,9 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.event { .event {
overflow-x: hidden; overflow-x: hidden;
margin: 4px 2px; margin: 4px 2px;
padding: 5px; padding: 5px;
border-bottom: 1px solid var(--calendarBorderColor); border-bottom: 1px solid $borderColor;
border-left: 4px solid var(--calendarBorderColor); border-left: 4px solid $borderColor;
font-size: 12px; font-size: 12px;
&:global(.colorImpaired) { &:global(.colorImpaired) {
@@ -17,10 +15,10 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
composes: link from '~Components/Link/Link.css'; composes: link from '~Components/Link/Link.css';
display: block; display: block;
color: var(--defaultColor); color: $defaultColor;
&:hover { &:hover {
color: var(--defaultColor); color: $defaultColor;
text-decoration: none; text-decoration: none;
} }
} }
@@ -31,7 +29,7 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
} }
.movieInfo { .movieInfo {
color: var(--calendarTextDim); color: $calendarTextDim;
} }
.movieTitle, .movieTitle,
@@ -42,7 +40,7 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
} }
.movieTitle { .movieTitle {
color: var(--calendarTextDimAlternate); color: #3a3f51;
font-size: $defaultFontSize; font-size: $defaultFontSize;
} }
@@ -55,85 +53,37 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
*/ */
.downloaded { .downloaded {
border-left-color: var(--successColor) !important; border-left-color: $successColor !important;
&:global(.fullColor) {
background-color: rgba(39, 194, 76, 0.4) !important;
}
&:global(.colorImpaired) { &:global(.colorImpaired) {
border-left-color: color(var(--successColor), saturation(+15%)) !important; border-left-color: color($successColor, saturation(+15%)) !important;
} }
} }
.queue { .queue {
border-left-color: var(--purple) !important; border-left-color: $purple !important;
&:global(.fullColor) {
background-color: rgba(122, 67, 182, 0.4) !important;
}
} }
.unmonitored { .unmonitored {
border-left-color: var(--gray) !important; border-left-color: $gray !important;
&:global(.fullColor) {
background-color: rgba(173, 173, 173, 0.5) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
} }
.missingUnmonitored { .missingUnmonitored {
border-left-color: var(--warningColor) !important; border-left-color: $warningColor !important;
&:global(.fullColor) {
background-color: rgba(255, 165, 0, 0.6) !important;
}
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px); background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
} }
} }
.missingMonitored { .missingMonitored {
border-left-color: var(--dangerColor) !important; border-left-color: $dangerColor !important;
&:global(.fullColor) {
background-color: rgba(240, 80, 80, 0.6) !important;
}
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px); background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
} }
} }
.unaired { .continuing {
border-left-color: var(--primaryColor) !important; border-left-color: $primaryColor !important;
&:global(.fullColor) {
background-color: rgba(93, 156, 236, 0.4) !important;
}
&:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
}
&:global(.fullColor.colorImpaired) {
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
}
} }
@@ -32,7 +32,6 @@ class CalendarEvent extends Component {
queueItem, queueItem,
showMovieInformation, showMovieInformation,
showCutoffUnmetIcon, showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode, colorImpairedMode,
date date
} = this.props; } = this.props;
@@ -63,8 +62,7 @@ class CalendarEvent extends Component {
styles.event, styles.event,
styles.link, styles.link,
styles[statusStyle], styles[statusStyle],
colorImpairedMode && 'colorImpaired', colorImpairedMode && 'colorImpaired'
fullColorEvents && 'fullColor'
)} )}
// component="div" // component="div"
to={link} to={link}
@@ -99,7 +97,7 @@ class CalendarEvent extends Component {
<Icon <Icon
className={styles.statusIcon} className={styles.statusIcon}
name={icons.MOVIE_FILE} name={icons.MOVIE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING} kind={kinds.WARNING}
title={translate('QualityCutoffHasNotBeenMet')} title={translate('QualityCutoffHasNotBeenMet')}
/> />
} }
@@ -144,12 +142,11 @@ CalendarEvent.propTypes = {
digitalRelease: PropTypes.string, digitalRelease: PropTypes.string,
monitored: PropTypes.bool.isRequired, monitored: PropTypes.bool.isRequired,
certification: PropTypes.string, certification: PropTypes.string,
hasFile: PropTypes.bool, hasFile: PropTypes.bool.isRequired,
grabbed: PropTypes.bool, grabbed: PropTypes.bool,
queueItem: PropTypes.object, queueItem: PropTypes.object,
showMovieInformation: PropTypes.bool.isRequired, showMovieInformation: PropTypes.bool.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired, showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
colorImpairedMode: PropTypes.bool.isRequired, colorImpairedMode: PropTypes.bool.isRequired,
date: PropTypes.string.isRequired date: PropTypes.string.isRequired
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import QueueDetails from 'Activity/Queue/QueueDetails'; import QueueDetails from 'Activity/Queue/QueueDetails';
import CircularProgressBar from 'Components/CircularProgressBar'; import CircularProgressBar from 'Components/CircularProgressBar';
import colors from 'Styles/Variables/colors';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
function CalendarEventQueueDetails(props) { function CalendarEventQueueDetails(props) {
@@ -34,7 +35,7 @@ function CalendarEventQueueDetails(props) {
progress={progress} progress={progress}
size={20} size={20}
strokeWidth={2} strokeWidth={2}
strokeColor={'#7a43b6'} strokeColor={colors.purple}
/> />
</div> </div>
} }
+1 -10
View File
@@ -9,7 +9,6 @@ import styles from './Legend.css';
function Legend(props) { function Legend(props) {
const { const {
showCutoffUnmetIcon, showCutoffUnmetIcon,
fullColorEvents,
colorImpairedMode colorImpairedMode
} = props; } = props;
@@ -20,7 +19,7 @@ function Legend(props) {
<LegendIconItem <LegendIconItem
name={translate('CutoffUnmet')} name={translate('CutoffUnmet')}
icon={icons.MOVIE_FILE} icon={icons.MOVIE_FILE}
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING} kind={kinds.WARNING}
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')} tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
/> />
); );
@@ -32,14 +31,12 @@ function Legend(props) {
<LegendItem <LegendItem
style='ended' style='ended'
name={translate('DownloadedAndMonitored')} name={translate('DownloadedAndMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
<LegendItem <LegendItem
style='availNotMonitored' style='availNotMonitored'
name={translate('DownloadedButNotMonitored')} name={translate('DownloadedButNotMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
</div> </div>
@@ -48,14 +45,12 @@ function Legend(props) {
<LegendItem <LegendItem
style='missingMonitored' style='missingMonitored'
name={translate('MissingMonitoredAndConsideredAvailable')} name={translate('MissingMonitoredAndConsideredAvailable')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
<LegendItem <LegendItem
style='missingUnmonitored' style='missingUnmonitored'
name={translate('MissingNotMonitored')} name={translate('MissingNotMonitored')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
</div> </div>
@@ -64,14 +59,12 @@ function Legend(props) {
<LegendItem <LegendItem
style='queue' style='queue'
name={translate('Queued')} name={translate('Queued')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
<LegendItem <LegendItem
style='continuing' style='continuing'
name={translate('Unreleased')} name={translate('Unreleased')}
fullColorEvents={fullColorEvents}
colorImpairedMode={colorImpairedMode} colorImpairedMode={colorImpairedMode}
/> />
</div> </div>
@@ -86,9 +79,7 @@ function Legend(props) {
} }
Legend.propTypes = { Legend.propTypes = {
view: PropTypes.string.isRequired,
showCutoffUnmetIcon: PropTypes.bool.isRequired, showCutoffUnmetIcon: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired colorImpairedMode: PropTypes.bool.isRequired
}; };
@@ -6,12 +6,10 @@ import Legend from './Legend';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state) => state.calendar.options, (state) => state.calendar.options,
(state) => state.calendar.view,
createUISettingsSelector(), createUISettingsSelector(),
(calendarOptions, view, uiSettings) => { (calendarOptions, uiSettings) => {
return { return {
...calendarOptions, ...calendarOptions,
view,
colorImpairedMode: uiSettings.enableColorImpairedMode colorImpairedMode: uiSettings.enableColorImpairedMode
}; };
} }
@@ -8,7 +8,6 @@ function LegendIconItem(props) {
name, name,
icon, icon,
kind, kind,
darken,
tooltip tooltip
} = props; } = props;
@@ -20,7 +19,6 @@ function LegendIconItem(props) {
<Icon <Icon
className={styles.icon} className={styles.icon}
name={icon} name={icon}
darken={darken}
kind={kind} kind={kind}
/> />
@@ -33,12 +31,7 @@ LegendIconItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
icon: PropTypes.object.isRequired, icon: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired, kind: PropTypes.string.isRequired,
darken: PropTypes.bool.isRequired,
tooltip: PropTypes.string.isRequired tooltip: PropTypes.string.isRequired
}; };
LegendIconItem.defaultProps = {
darken: false
};
export default LegendIconItem; export default LegendIconItem;
+10 -14
View File
@@ -1,5 +1,3 @@
$fullColorGradient: rgba(244, 245, 246, 0.2);
.legendItemContainer { .legendItemContainer {
margin-right: 5px; margin-right: 5px;
width: 220px; width: 220px;
@@ -22,55 +20,53 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
.queue { .queue {
composes: legendItemColor; composes: legendItemColor;
background-color: var(--queueColor); background-color: $queueColor;
} }
.continuing { .continuing {
composes: legendItemColor; composes: legendItemColor;
background-color: var(--primaryColor); background-color: $primaryColor;
} }
.availNotMonitored { .availNotMonitored {
composes: legendItemColor; composes: legendItemColor;
background-color: var(--darkGray); background-color: $darkGray;
} }
.ended { .ended {
composes: legendItemColor; composes: legendItemColor;
background-color: var(--successColor); background-color: $successColor;
} }
.missingMonitored { .missingMonitored {
composes: legendItemColor; composes: legendItemColor;
background-color: var(--dangerColor); background-color: $dangerColor;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(90deg, color(var(--dangerColor) shade(5%)), color(var(--dangerColor) shade(5%)) 5px, color(var(--dangerColor) shade(15%)) 5px, color(var(--dangerColor) shade(15%)) 10px); background: repeating-linear-gradient(90deg, color($dangerColor shade(5%)), color($dangerColor shade(5%)) 5px, color($dangerColor shade(15%)) 5px, color($dangerColor shade(15%)) 10px);
} }
} }
.missingUnmonitored { .missingUnmonitored {
composes: legendItemColor; composes: legendItemColor;
background-color: var(--warningColor); background-color: $warningColor;
&:global(.colorImpaired) { &:global(.colorImpaired) {
background: repeating-linear-gradient(45deg, var(--warningColor), var(--warningColor) 5px, color(var(--warningColor) tint(15%)) 5px, color(var(--warningColor) tint(15%)) 10px); background: repeating-linear-gradient(45deg, $warningColor, $warningColor 5px, color($warningColor tint(15%)) 5px, color($warningColor tint(15%)) 10px);
} }
} }
.missingMonitoredColorImpaired { .missingMonitoredColorImpaired {
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px); background: repeating-linear-gradient(90deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
color: var(--white);
} }
.missingUnmonitoredColorImpaired { .missingUnmonitoredColorImpaired {
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px); background: repeating-linear-gradient(45deg, $colorImpairedGradientDark, $colorImpairedGradientDark 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
color: var(--white);
} }
.legendItemText { .legendItemText {
+1 -4
View File
@@ -7,7 +7,6 @@ function LegendItem(props) {
const { const {
name, name,
style, style,
fullColorEvents,
colorImpairedMode colorImpairedMode
} = props; } = props;
@@ -17,8 +16,7 @@ function LegendItem(props) {
className={classNames( className={classNames(
styles.legendItem, styles.legendItem,
styles[style], styles[style],
colorImpairedMode && 'colorImpaired', colorImpairedMode && 'colorImpaired'
fullColorEvents && 'fullColor'
)} )}
/> />
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}> <div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}>
@@ -31,7 +29,6 @@ function LegendItem(props) {
LegendItem.propTypes = { LegendItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
style: PropTypes.string.isRequired, style: PropTypes.string.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired colorImpairedMode: PropTypes.bool.isRequired
}; };
@@ -26,16 +26,14 @@ class CalendarOptionsModalContent extends Component {
firstDayOfWeek, firstDayOfWeek,
calendarWeekColumnHeader, calendarWeekColumnHeader,
timeFormat, timeFormat,
enableColorImpairedMode, enableColorImpairedMode
fullColorEvents
} = props; } = props;
this.state = { this.state = {
firstDayOfWeek, firstDayOfWeek,
calendarWeekColumnHeader, calendarWeekColumnHeader,
timeFormat, timeFormat,
enableColorImpairedMode, enableColorImpairedMode
fullColorEvents
}; };
} }
@@ -96,7 +94,6 @@ class CalendarOptionsModalContent extends Component {
const { const {
showMovieInformation, showMovieInformation,
showCutoffUnmetIcon, showCutoffUnmetIcon,
fullColorEvents,
onModalClose onModalClose
} = this.props; } = this.props;
@@ -139,18 +136,6 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onOptionInputChange} onChange={this.onOptionInputChange}
/> />
</FormGroup> </FormGroup>
<FormGroup>
<FormLabel>{translate('FullColorEvents')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="fullColorEvents"
value={fullColorEvents}
helpText={translate('FullColorEventsHelpText')}
onChange={this.onOptionInputChange}
/>
</FormGroup>
</Form> </Form>
</FieldSet> </FieldSet>
@@ -177,7 +162,7 @@ class CalendarOptionsModalContent extends Component {
values={weekColumnOptions} values={weekColumnOptions}
value={calendarWeekColumnHeader} value={calendarWeekColumnHeader}
onChange={this.onGlobalInputChange} onChange={this.onGlobalInputChange}
helpText={translate('SettingsWeekColumnHeaderHelpText')} helpText={translate('HelpText')}
/> />
</FormGroup> </FormGroup>
@@ -191,9 +176,7 @@ class CalendarOptionsModalContent extends Component {
value={timeFormat} value={timeFormat}
onChange={this.onGlobalInputChange} onChange={this.onGlobalInputChange}
/> />
</FormGroup> </FormGroup><FormGroup>
<FormGroup>
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel> <FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
<FormInputGroup <FormInputGroup
@@ -204,6 +187,7 @@ class CalendarOptionsModalContent extends Component {
onChange={this.onGlobalInputChange} onChange={this.onGlobalInputChange}
/> />
</FormGroup> </FormGroup>
</Form> </Form>
</FieldSet> </FieldSet>
</ModalBody> </ModalBody>
@@ -225,7 +209,6 @@ CalendarOptionsModalContent.propTypes = {
calendarWeekColumnHeader: PropTypes.string.isRequired, calendarWeekColumnHeader: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
enableColorImpairedMode: PropTypes.bool.isRequired, enableColorImpairedMode: PropTypes.bool.isRequired,
fullColorEvents: PropTypes.bool.isRequired,
dispatchSetCalendarOption: PropTypes.func.isRequired, dispatchSetCalendarOption: PropTypes.func.isRequired,
dispatchSaveUISettings: PropTypes.func.isRequired, dispatchSaveUISettings: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
@@ -22,7 +22,7 @@ function getUrls(state) {
tags tags
} = state; } = state;
let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v4/calendar/Radarr.ics?`; let icalUrl = `${window.location.host}${window.Radarr.urlBase}/feed/v3/calendar/Radarr.ics?`;
if (unmonitored) { if (unmonitored) {
icalUrl += 'unmonitored=true&'; icalUrl += 'unmonitored=true&';
@@ -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;
@@ -1,68 +0,0 @@
.container {
display: flex;
}
.year {
margin-left: 5px;
color: var(--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;
}
}
@@ -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,
qualityProfileIds,
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('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
onChange={this.onQualityProfileIdChange}
{...qualityProfileIds}
/>
</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,
qualityProfileIds: 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;
@@ -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',
qualityProfileIds: collection.qualityProfileIds,
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,
qualityProfileIds,
minimumAvailability,
searchForMovie,
tags
} = this.props;
this.props.addMovie({
tmdbId,
title,
rootFolderPath: rootFolderPath.value,
monitor: monitor.value,
qualityProfileIds: qualityProfileIds.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,
qualityProfileIds: 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);
-403
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;
@@ -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'
);
@@ -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);
@@ -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;
}
}
-236
View File
@@ -1,236 +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,
rootFolderPath
} = 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;
}
if (rootFolderPath !== NO_CHANGE) {
changes.rootFolderPath = rootFolderPath;
}
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;
@@ -1,8 +0,0 @@
.label {
margin-bottom: 3px;
font-weight: bold;
}
.savingIcon {
margin-left: 8px;
}
@@ -1,40 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import SpinnerIcon from 'Components/SpinnerIcon';
import { icons } from 'Helpers/Props';
import styles from './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;
@@ -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);
@@ -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;
@@ -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);
@@ -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;
}
@@ -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,
qualityProfileIds,
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('QualityProfiles')}</FormLabel>
<FormInputGroup
type={inputTypes.QUALITY_PROFILE_SELECT}
name="qualityProfileIds"
{...qualityProfileIds}
onChange={onInputChange}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('RootFolder')}</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;
@@ -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,
qualityProfileIds: collection.qualityProfileIds,
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);
@@ -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;
@@ -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;
-11
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;
}
-52
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;
@@ -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 var(--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: var(--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: var(--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: var(--white);
font-size: $smallFontSize;
opacity: 0;
transition: opacity 0;
}
.action {
composes: button from '~Components/Link/IconButton.css';
&:hover {
color: var(--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: var(--defaultColor);
}
.monitorToggleButton {
composes: toggleButton from '~Components/MonitorToggleButton.css';
width: 25px;
color: var(--white);
&:hover {
color: var(--iconButtonHoverLightColor);
}
}
@@ -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;
@@ -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);
@@ -1,50 +0,0 @@
.movie {
display: flex;
align-items: stretch;
overflow: hidden;
margin: 2px 4px;
border: 1px solid var(--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: var(--white);
color: var(--defaultColor);
}
.primary {
border-color: var(--primaryColor);
}
.danger {
border-color: var(--dangerColor);
}
.success {
border-color: var(--successColor);
}
.purple {
border-color: var(--purple);
}
.warning {
border-color: var(--warningColor);
}
.info {
border-color: var(--infoColor);
}
.queue {
border-color: var(--queueColor);
}
@@ -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,
onMonitorTogglePress,
isSaving,
statistics
} = this.props;
const { movieFileCount } = statistics;
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, movieFileCount > 0, isAvailable, 'kinds')]
)}
>
{
movieFileCount > 0 ? translate('Downloaded') : translate('Missing')
}
</div>
}
</div>
);
}
}
CollectionMovieLabel.propTypes = {
id: PropTypes.number,
title: PropTypes.string.isRequired,
status: PropTypes.string,
statistics: PropTypes.object.isRequired,
isAvailable: PropTypes.bool,
monitored: PropTypes.bool,
isSaving: PropTypes.bool.isRequired,
movieFile: PropTypes.object,
movieFileId: PropTypes.number,
onMonitorTogglePress: PropTypes.func.isRequired
};
CollectionMovieLabel.defaultProps = {
isSaving: false,
statistics: {
movieFileCount: 0
}
};
export default CollectionMovieLabel;
@@ -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);
@@ -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: var(--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: var(--iconButtonHoverLightColor);
}
}
}
@@ -1,353 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextTruncate from 'react-text-truncate';
import { Navigation } from 'swiper';
import { Swiper, SwiperSlide } from 'swiper/react';
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 Swiper styles
import 'swiper/css';
import 'swiper/css/navigation';
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
setSliderPrevRef = (ref) => {
this._swiperPrevRef = ref;
};
setSliderNextRef = (ref) => {
this._swiperNextRef = 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,
qualityProfileIds,
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;
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}>
<span ref={this.setSliderPrevRef}>
<IconButton
name={icons.ARROW_LEFT}
title={translate('ScrollMovies')}
size={20}
/>
</span>
<span ref={this.setSliderNextRef}>
<IconButton
name={icons.ARROW_RIGHT}
title={translate('ScrollMovies')}
size={20}
/>
</span>
</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
qualityProfileIds={qualityProfileIds}
/>
}
</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}>
<Swiper
slidesPerView='auto'
spaceBetween={10}
slidesPerGroup={3}
loop={false}
loopFillGroupWithBlank={true}
className="mySwiper"
modules={[Navigation]}
onInit={(swiper) => {
swiper.params.navigation.prevEl = this._swiperPrevRef;
swiper.params.navigation.nextEl = this._swiperNextRef;
swiper.navigation.init();
swiper.navigation.update();
}}
>
{movies.map((movie) => (
<SwiperSlide key={movie.tmdbId} style={{ width: posterWidth }}>
<CollectionMovieConnector
key={movie.tmdbId}
posterWidth={posterWidth}
posterHeight={posterHeight}
detailedProgressBar={detailedProgressBar}
collectionId={id}
{...movie}
/>
</SwiperSlide>
))}
</Swiper>
</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,
qualityProfileIds: 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;
@@ -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);
@@ -1,15 +0,0 @@
.grid {
flex: 1 0 auto;
}
.container {
&:hover {
.content {
background-color: var(--tableRowHoverBackgroundColor);
}
}
}
.externalLinks {
margin-right: 0.5em;
}
@@ -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;
@@ -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);
@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import CollectionOverviewOptionsModalContentConnector from './CollectionOverviewOptionsModalContentConnector';
function CollectionOverviewOptionsModal({ isOpen, onModalClose, ...otherProps }) {
return (
<Modal
isOpen={isOpen}
onModalClose={onModalClose}
>
<CollectionOverviewOptionsModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
CollectionOverviewOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CollectionOverviewOptionsModal;
@@ -1,205 +0,0 @@
import _ from 'lodash';
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 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 translate from 'Utilities/String/translate';
const posterSizeOptions = [
{ key: 'small', value: translate('Small') },
{ key: 'medium', value: translate('Medium') },
{ key: 'large', value: translate('Large') }
];
class CollectionOverviewOptionsModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
detailedProgressBar: props.detailedProgressBar,
size: props.size,
showDetails: props.showDetails,
showOverview: props.showOverview,
showPosters: props.showPosters
};
}
componentDidUpdate(prevProps) {
const {
detailedProgressBar,
size,
showDetails,
showOverview,
showPosters
} = this.props;
const state = {};
if (detailedProgressBar !== prevProps.detailedProgressBar) {
state.detailedProgressBar = detailedProgressBar;
}
if (size !== prevProps.size) {
state.size = size;
}
if (showDetails !== prevProps.showDetails) {
state.showDetails = showDetails;
}
if (showOverview !== prevProps.showOverview) {
state.showOverview = showOverview;
}
if (showPosters !== prevProps.showPosters) {
state.showPosters = showPosters;
}
if (!_.isEmpty(state)) {
this.setState(state);
}
}
//
// Listeners
onChangeOverviewOption = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onChangeOverviewOption({ [name]: value });
});
};
onChangeOption = ({ name, value }) => {
this.setState({
[name]: value
}, () => {
this.props.onChangeOption({
[name]: value
});
});
};
//
// Render
render() {
const {
onModalClose
} = this.props;
const {
size,
detailedProgressBar,
showDetails,
showPosters,
showOverview
} = this.state;
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('CollectionOptions')}
</ModalHeader>
<ModalBody>
<Form>
<FormGroup>
<FormLabel>{translate('PosterSize')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="size"
value={size}
values={posterSizeOptions}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('DetailedProgressBar')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="detailedProgressBar"
value={detailedProgressBar}
helpText={translate('DetailedProgressBarHelpText')}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowCollectionDetails')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showDetails"
value={showDetails}
helpText={translate('CollectionShowDetailsHelpText')}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowOverview')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showOverview"
value={showOverview}
helpText={translate('CollectionShowOverviewsHelpText')}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
<FormGroup>
<FormLabel>{translate('ShowPosters')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="showPosters"
value={showPosters}
helpText={translate('CollectionShowPostersHelpText')}
onChange={this.onChangeOverviewOption}
/>
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button
onPress={onModalClose}
>
{translate('Close')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
CollectionOverviewOptionsModalContent.propTypes = {
detailedProgressBar: PropTypes.bool.isRequired,
size: PropTypes.string.isRequired,
showDetails: PropTypes.bool.isRequired,
showOverview: PropTypes.bool.isRequired,
showPosters: PropTypes.bool.isRequired,
onChangeOverviewOption: PropTypes.func.isRequired,
onChangeOption: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default CollectionOverviewOptionsModalContent;
@@ -1,29 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { setMovieCollectionsOption, setMovieCollectionsOverviewOption } from 'Store/Actions/movieCollectionActions';
import CollectionOverviewOptionsModalContent from './CollectionOverviewOptionsModalContent';
function createMapStateToProps() {
return createSelector(
(state) => state.movieCollections,
(movieCollections) => {
return {
...movieCollections.options,
...movieCollections.overviewOptions
};
}
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onChangeOverviewOption(payload) {
dispatch(setMovieCollectionsOverviewOption(payload));
},
onChangeOption(payload) {
dispatch(setMovieCollectionsOption(payload));
}
};
}
export default connect(createMapStateToProps, createMapDispatchToProps)(CollectionOverviewOptionsModalContent);
-2
View File
@@ -10,12 +10,10 @@ export const DOWNLOADED_MOVIES_SCAN = 'DownloadedMoviesScan';
export const INTERACTIVE_IMPORT = 'ManualImport'; export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch'; export const MISSING_MOVIES_SEARCH = 'MissingMoviesSearch';
export const MOVE_MOVIE = 'MoveMovie'; export const MOVE_MOVIE = 'MoveMovie';
export const REFRESH_COLLECTIONS = 'RefreshCollections';
export const REFRESH_MOVIE = 'RefreshMovie'; export const REFRESH_MOVIE = 'RefreshMovie';
export const RENAME_FILES = 'RenameFiles'; export const RENAME_FILES = 'RenameFiles';
export const RENAME_MOVIE = 'RenameMovie'; export const RENAME_MOVIE = 'RenameMovie';
export const RESET_API_KEY = 'ResetApiKey'; export const RESET_API_KEY = 'ResetApiKey';
export const RESET_QUALITY_DEFINITIONS = 'ResetQualityDefinitions';
export const RSS_SYNC = 'RssSync'; export const RSS_SYNC = 'RssSync';
export const MOVIE_SEARCH = 'MoviesSearch'; export const MOVIE_SEARCH = 'MoviesSearch';
export const IMPORT_LIST_SYNC = 'ImportListSync'; export const IMPORT_LIST_SYNC = 'ImportListSync';
+12 -12
View File
@@ -7,25 +7,25 @@
} }
.danger { .danger {
border-color: var(--alertDangerBorderColor); border-color: $alertDangerBorderColor;
background-color: var(--alertDangerBackgroundColor); background-color: $alertDangerBackgroundColor;
color: var(--alertDangerColor); color: $alertDangerColor;
} }
.info { .info {
border-color: var(--alertInfoBorderColor); border-color: $alertInfoBorderColor;
background-color: var(--alertInfoBackgroundColor); background-color: $alertInfoBackgroundColor;
color: var(--alertInfoColor); color: $alertInfoColor;
} }
.success { .success {
border-color: var(--alertSuccessBorderColor); border-color: $alertSuccessBorderColor;
background-color: var(--alertSuccessBackgroundColor); background-color: $alertSuccessBackgroundColor;
color: var(--alertSuccessColor); color: $alertSuccessColor;
} }
.warning { .warning {
border-color: var(--alertWarningBorderColor); border-color: $alertWarningBorderColor;
background-color: var(--alertWarningBackgroundColor); background-color: $alertWarningBackgroundColor;
color: var(--alertWarningColor); color: $alertWarningColor;
} }
+3 -3
View File
@@ -3,9 +3,9 @@
margin: 10px; margin: 10px;
padding: 10px; padding: 10px;
border-radius: 3px; border-radius: 3px;
background-color: var(--cardBackgroundColor); background-color: $white;
box-shadow: 0 0 10px 1px var(--cardShadowColor); box-shadow: 0 0 10px 1px $cardShadowColor;
color: var(--defaultColor); color: $defaultColor;
} }
.underlay { .underlay {
@@ -1,5 +1,6 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import colors from 'Styles/Variables/colors';
import styles from './CircularProgressBar.css'; import styles from './CircularProgressBar.css';
class CircularProgressBar extends Component { class CircularProgressBar extends Component {
@@ -131,7 +132,7 @@ CircularProgressBar.defaultProps = {
containerClassName: styles.circularProgressBarContainer, containerClassName: styles.circularProgressBarContainer,
size: 60, size: 60,
strokeWidth: 5, strokeWidth: 5,
strokeColor: '#ffc230', strokeColor: colors.radarrYellow,
showProgressText: false showProgressText: false
}; };
+1 -1
View File
@@ -13,7 +13,7 @@
width: 100%; width: 100%;
border: 0; border: 0;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
color: var(--textColor); color: #3a3f51;
font-size: 21px; font-size: 21px;
line-height: inherit; line-height: inherit;

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