mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-11 15:20:34 -04:00
Compare commits
127 Commits
aphrodite
...
new-exclus
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
793cf22c3b | ||
|
|
76cabb4927 | ||
|
|
5b83d09d5e | ||
|
|
82eadcffaa | ||
|
|
1ebb71db59 | ||
|
|
662b3894c2 | ||
|
|
a1c21af9b5 | ||
|
|
67fca87c44 | ||
|
|
6ee2780370 | ||
|
|
0086e2699e | ||
|
|
37c9701237 | ||
|
|
6d452d8479 | ||
|
|
e82f0c01d8 | ||
|
|
ee456c3291 | ||
|
|
9986f0119b | ||
|
|
5d4da26195 | ||
|
|
5ed448c930 | ||
|
|
9263e31b7b | ||
|
|
516122c6f3 | ||
|
|
256a50abab | ||
|
|
315929bc5e | ||
|
|
4a681601b2 | ||
|
|
ab3b5bdf8b | ||
|
|
e52288bd67 | ||
|
|
f2f26d88b9 | ||
|
|
27354507cb | ||
|
|
b7aa1df219 | ||
|
|
2d7942d69c | ||
|
|
0a8dd85856 | ||
|
|
f917d0e9bc | ||
|
|
024e4df99c | ||
|
|
49fa402c55 | ||
|
|
02c95658c4 | ||
|
|
3bc4231640 | ||
|
|
a9a0d47f9f | ||
|
|
0dd05b2dac | ||
|
|
3986433884 | ||
|
|
f637976530 | ||
|
|
603d26bb5f | ||
|
|
6b41ad7442 | ||
|
|
4c049ac3d9 | ||
|
|
84df3e8b5b | ||
|
|
0ff76e14bb | ||
|
|
5433c6364c | ||
|
|
038e3d5c44 | ||
|
|
71b126024b | ||
|
|
76565d4ab5 | ||
|
|
28c15bc425 | ||
|
|
aeda4cba32 | ||
|
|
a826c1dc25 | ||
|
|
00022fd206 | ||
|
|
0647663f46 | ||
|
|
b83bfb045b | ||
|
|
cfbac482e5 | ||
|
|
b43b35a295 | ||
|
|
e75d52de0e | ||
|
|
f850e75d4e | ||
|
|
6c9e4994d8 | ||
|
|
708a2e31d5 | ||
|
|
37c1b5b28c | ||
|
|
e28bea14b3 | ||
|
|
2823099237 | ||
|
|
53eeee8b91 | ||
|
|
e880eb0e00 | ||
|
|
561f84adff | ||
|
|
4fc6a14d1b | ||
|
|
b8d5a0b6a2 | ||
|
|
73b0a461d3 | ||
|
|
d86402efb1 | ||
|
|
1c892d7357 | ||
|
|
f7e21ec2a4 | ||
|
|
1d0771c9a4 | ||
|
|
cc384d9297 | ||
|
|
7898100d95 | ||
|
|
4b279e87cf | ||
|
|
3d5570dfd9 | ||
|
|
269462e0a2 | ||
|
|
5799b3dc47 | ||
|
|
dfbbb7d9bd | ||
|
|
6faa484d4e | ||
|
|
86363d5bf1 | ||
|
|
30c51ec4f3 | ||
|
|
a66b2cf416 | ||
|
|
37197150be | ||
|
|
c8bbd21615 | ||
|
|
fdf2d1c9b3 | ||
|
|
eb4cd9633a | ||
|
|
3fa17bf7f6 | ||
|
|
22e9dff76b | ||
|
|
7d31eb1f55 | ||
|
|
4ec71538b9 | ||
|
|
295b975046 | ||
|
|
0198c7a3b1 | ||
|
|
088ffc34df | ||
|
|
1740ad337c | ||
|
|
a6758e4bf4 | ||
|
|
2161f08140 | ||
|
|
1a92372506 | ||
|
|
5bd23be133 | ||
|
|
eca816db86 | ||
|
|
c4f19a813d | ||
|
|
24cee7e4fe | ||
|
|
22531294be | ||
|
|
464d92bb70 | ||
|
|
8ee16b81ec | ||
|
|
a7a1d48e0d | ||
|
|
a070279993 | ||
|
|
95918c4053 | ||
|
|
646b86f8c9 | ||
|
|
28835a1857 | ||
|
|
6b22481a00 | ||
|
|
aef8a8fd04 | ||
|
|
a1e69c3c2b | ||
|
|
995d257d3d | ||
|
|
cf804f7dac | ||
|
|
081fe64bff | ||
|
|
9075fdc1c1 | ||
|
|
5db1faad0b | ||
|
|
57f3805763 | ||
|
|
6bdd24e62d | ||
|
|
d5ec2914e2 | ||
|
|
d8a0aac9c3 | ||
|
|
987ed357d5 | ||
|
|
9044976393 | ||
|
|
b53def3da5 | ||
|
|
2ecb988c6a | ||
|
|
41cf722ab5 |
@@ -185,17 +185,9 @@ dotnet_diagnostic.CA1814.severity = suggestion
|
||||
dotnet_diagnostic.CA1815.severity = suggestion
|
||||
dotnet_diagnostic.CA1816.severity = suggestion
|
||||
dotnet_diagnostic.CA1819.severity = suggestion
|
||||
dotnet_diagnostic.CA1820.severity = suggestion
|
||||
dotnet_diagnostic.CA1821.severity = suggestion
|
||||
dotnet_diagnostic.CA1822.severity = suggestion
|
||||
dotnet_diagnostic.CA1823.severity = suggestion
|
||||
dotnet_diagnostic.CA1824.severity = suggestion
|
||||
dotnet_diagnostic.CA1825.severity = suggestion
|
||||
dotnet_diagnostic.CA1826.severity = suggestion
|
||||
dotnet_diagnostic.CA1827.severity = suggestion
|
||||
dotnet_diagnostic.CA1828.severity = suggestion
|
||||
dotnet_diagnostic.CA1829.severity = suggestion
|
||||
dotnet_diagnostic.CA1834.severity = suggestion
|
||||
dotnet_diagnostic.CA2000.severity = suggestion
|
||||
dotnet_diagnostic.CA2002.severity = suggestion
|
||||
dotnet_diagnostic.CA2007.severity = suggestion
|
||||
|
||||
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -6,7 +6,7 @@
|
||||
|
||||
**Just because you receive an exception in your logs, doesn't mean it's a bug and should be reported here. Often it's something else, such as a permission error. If you are unsure ask on the Discord or Subreddit first.**
|
||||
|
||||
Visit our [Discord server](https://discord.gg/NWYch8M) or [Subreddit](https://reddit.com/r/radarr) for support or longer discussions. Support questions posed on here will be closed immediately.
|
||||
Visit our [Discord server](https://discord.gg/r5wJPt9) or [Subreddit](https://reddit.com/r/radarr) for support or longer discussions. Support questions posed on here will be closed immediately.
|
||||
|
||||
Provide a description of the feature request or bug here, the more details the better.
|
||||
Please also include the following if you are reporting a bug. If you do not include it, the issue will probably be closed as we cannot help you. -->
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,7 +1,7 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Support via Discord
|
||||
url: https://discord.gg/AD3UP37
|
||||
url: https://discord.gg/r5wJPt9
|
||||
about: Chat with users and devs on support and setup related topics.
|
||||
- name: Support via Reddit
|
||||
url: https://reddit.com/r/radarr
|
||||
|
||||
5
.github/stale.yml
vendored
5
.github/stale.yml
vendored
@@ -7,7 +7,10 @@ exemptLabels:
|
||||
- feature request
|
||||
- parser
|
||||
- confirmed
|
||||
- aphrodite
|
||||
- sonarr-pull
|
||||
- lidarr-pull
|
||||
- readarr-pull
|
||||
- v3
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
|
||||
2
.github/support.yml
vendored
2
.github/support.yml
vendored
@@ -6,7 +6,7 @@ supportLabel: support
|
||||
# to a support page, or set to `false` to disable
|
||||
supportComment: >
|
||||
We use the issue tracker exclusively for bug reports and feature requests.
|
||||
However, this issue appears to be a support request. Please hop over onto our [Discord](https://discord.gg/ZDmT7qb) or [Subreddit](https://reddit.com/r/radarr)
|
||||
However, this issue appears to be a support request. Please hop over onto our [Discord](https://discord.gg/r5wJPt9) or [Subreddit](https://reddit.com/r/radarr)
|
||||
# Whether to close issues marked as support requests
|
||||
close: true
|
||||
# Whether to lock issues marked as support requests
|
||||
|
||||
@@ -14,7 +14,7 @@ See the readme for information on setting up your development environment.
|
||||
- Rebase from Radarr's develop branch, don't merge
|
||||
- Make meaningful commits, or squash them
|
||||
- Feel free to make a pull request before work is complete, this will let us see where its at and make comments/suggest improvements
|
||||
- Reach out to us on the forums or on IRC if you have any questions
|
||||
- Reach out to us on the discord if you have any questions
|
||||
- Add tests (unit/integration)
|
||||
- Commit with *nix line endings for consistency (We checkout Windows and commit *nix)
|
||||
- One feature/bug fix per pull request to keep things clean and easy to understand
|
||||
|
||||
15
README.md
15
README.md
@@ -28,15 +28,18 @@ If you are using Docker please ensure your Docker paths are setup correctly usin
|
||||
## Downloads
|
||||
Please note that v0.2 will only have critical bugs resolved as of August 2020. Any additional development or features will be soley in V3.
|
||||
|
||||
| Release Type | Branch: develop (stable) | Branch: nightly (semi-unstable) | Branch: aphrodite (very-unstable) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Binary Releases | [](https://github.com/Radarr/Radarr/releases) | [](https://ci.appveyor.com/project/galli-leo/radarr-usby1/branch/develop/artifacts) | |
|
||||
| Docker - lsio | [](https://hub.docker.com/r/linuxserver/radarr) | [](https://hub.docker.com/r/linuxserver/radarr) | [](https://hub.docker.com/r/linuxserver/radarr) |
|
||||
| Docker - hotio | [](https://hub.docker.com/r/hotio/radarr) | [](https://hub.docker.com/r/hotio/radarr) | [](https://hub.docker.com/r/hotio/radarr) |
|
||||
Each push to the "develop" branch creates a build on "nightly" release channel (release channel is the "branch" within radarr's settings), once we push a build to Github it will show up on "develop" release channel.
|
||||
|
||||
|
||||
| Release Channel Type | Branch: develop (stable) (v0.2) | Branch: nightly (semi-unstable) (v3.0) |
|
||||
|-----------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Binary Releases | [](https://github.com/Radarr/Radarr/releases) | [](https://radarr.servarr.com/v1/update/nightly/updatefile?os=windows&runtime=netcore&arch=x64) <br> [](https://radarr.servarr.com/v1/update/nightly/updatefile?os=linux&runtime=netcore&arch=x64) <br> [](https://radarr.servarr.com/v1/update/nightly/updatefile?os=linux&runtime=netcore&arch=arm64) [](https://radarr.servarr.com/v1/update/nightly/updatefile?os=linux&runtime=netcore&arch=arm) <br> [](https://radarr.servarr.com/v1/update/nightly/updatefile?os=osx&runtime=netcore&arch=x64)
|
||||
| Docker - lsio | [](https://hub.docker.com/r/linuxserver/radarr) | [](https://hub.docker.com/r/linuxserver/radarr) |
|
||||
| Docker - hotio | [](https://hub.docker.com/r/hotio/radarr) | [](https://hub.docker.com/r/hotio/radarr) |
|
||||
|
||||
## Support
|
||||
|
||||
[](https://discord.gg/AD3UP37)
|
||||
[](https://discord.gg/r5wJPt9)
|
||||
[](https://www.reddit.com/r/radarr)
|
||||
[](https://github.com/Radarr/Radarr/issues)
|
||||
[](https://github.com/Radarr/Radarr/wiki)
|
||||
|
||||
@@ -20,11 +20,10 @@ trigger:
|
||||
branches:
|
||||
include:
|
||||
- develop
|
||||
- aphrodite
|
||||
- master
|
||||
|
||||
pr:
|
||||
- develop
|
||||
- aphrodite
|
||||
|
||||
stages:
|
||||
- stage: Setup
|
||||
@@ -40,7 +39,7 @@ stages:
|
||||
displayName: Set Build Name
|
||||
- bash: |
|
||||
if [[ $BUILD_REASON == "PullRequest" ]]; then
|
||||
git diff origin/aphrodite...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
|
||||
git diff origin/develop...HEAD --name-only | grep -E "^(src/|azure-pipelines.yml)"
|
||||
echo $? > not_backend_update
|
||||
else
|
||||
echo 0 > not_backend_update
|
||||
@@ -302,14 +301,22 @@ stages:
|
||||
sentry-cli releases new --finalize -p radarr -p radarr-ui -p radarr-update "${RELEASENAME}"
|
||||
sentry-cli releases -p radarr-ui files "${RELEASENAME}" upload-sourcemaps _output/UI/ --rewrite
|
||||
sentry-cli releases set-commits --auto "${RELEASENAME}"
|
||||
sentry-cli releases deploys "${RELEASENAME}" new -e aphrodite
|
||||
if [[ ${BUILD_SOURCEBRANCH} == "refs/heads/develop" ]]; then
|
||||
sentry-cli releases deploys "${RELEASENAME}" new -e nightly
|
||||
else
|
||||
sentry-cli releases deploys "${RELEASENAME}" new -e production
|
||||
fi
|
||||
if [ $? -gt 0 ]; then
|
||||
echo "##vso[task.logissue type=warning]Error uploading source maps."
|
||||
fi
|
||||
exit 0
|
||||
displayName: Publish Sentry Source Maps
|
||||
continueOnError: true
|
||||
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/aphrodite'))
|
||||
condition: |
|
||||
or
|
||||
(
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop')),
|
||||
and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/master'))
|
||||
)
|
||||
env:
|
||||
SENTRY_AUTH_TOKEN: $(sentryAuthTokenServarr)
|
||||
SENTRY_ORG: $(sentryOrg)
|
||||
@@ -379,11 +386,6 @@ stages:
|
||||
- powershell: Set-Service SCardSvr -StartupType Manual
|
||||
displayName: Enable Windows Test Service
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
wget https://github.com/acoustid/chromaprint/releases/download/v1.4.3/chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz
|
||||
sudo tar xf chromaprint-fpcalc-1.4.3-linux-x86_64.tar.gz --strip-components=1 --directory /usr/bin
|
||||
displayName: Install fpcalc
|
||||
condition: and(succeeded(), eq(variables['osName'], 'Linux'))
|
||||
- bash: |
|
||||
SYMLINK=6_6_0
|
||||
MONOPREFIX=/Library/Frameworks/Mono.framework/Versions/$SYMLINK
|
||||
@@ -686,24 +688,21 @@ stages:
|
||||
mkdir -p ./bin/
|
||||
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Radarr/. ./bin/
|
||||
displayName: Move Package Contents
|
||||
- bash: |
|
||||
if [[ $OSNAME == "Mac" ]]; then
|
||||
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-macos.tar.gz
|
||||
elif [[ $OSNAME == "Linux" ]]; then
|
||||
url=https://github.com/mozilla/geckodriver/releases/download/v0.27.0/geckodriver-v0.27.0-linux64.tar.gz
|
||||
else
|
||||
echo "Unhandled OS"
|
||||
exit 1
|
||||
fi
|
||||
curl -s -L "$url" | tar -xz
|
||||
chmod +x geckodriver
|
||||
mv geckodriver _tests
|
||||
displayName: Install Gecko Driver
|
||||
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
|
||||
- bash: |
|
||||
chmod a+x ${TESTSFOLDER}/test.sh
|
||||
${TESTSFOLDER}/test.sh ${OSNAME} Automation Test
|
||||
displayName: Run Automation Tests
|
||||
- task: CopyFiles@2
|
||||
displayName: 'Copy Screenshot to: $(Build.ArtifactStagingDirectory)'
|
||||
inputs:
|
||||
SourceFolder: '$(Build.SourcesDirectory)'
|
||||
Contents: |
|
||||
**/*_test_screenshot.png
|
||||
TargetFolder: '$(Build.ArtifactStagingDirectory)/screenshots'
|
||||
- publish: $(Build.ArtifactStagingDirectory)/screenshots
|
||||
artifact: '$(osName)AutomationScreenshots'
|
||||
displayName: Publish Screenshot Bundle
|
||||
condition: and(succeeded(), eq(variables['System.JobAttempt'], '1'))
|
||||
- task: PublishTestResults@2
|
||||
inputs:
|
||||
testResultsFormat: 'NUnit'
|
||||
@@ -852,8 +851,15 @@ stages:
|
||||
- job:
|
||||
displayName: Discord Notification
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: 'windows-2019'
|
||||
steps:
|
||||
- task: DownloadPipelineArtifact@2
|
||||
continueOnError: true
|
||||
displayName: Download Screenshot Artifact
|
||||
inputs:
|
||||
buildType: 'current'
|
||||
artifactName: 'WindowsAutomationScreenshots'
|
||||
targetPath: $(Build.SourcesDirectory)
|
||||
- checkout: none
|
||||
- powershell: |
|
||||
iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/Servarr/AzureDiscordNotify/master/DiscordNotify.ps1'))
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"function-parentheses-newline-inside": "never-multi-line",
|
||||
"function-parentheses-space-inside": "never",
|
||||
"function-url-quotes": "always",
|
||||
"function-url-scheme-blacklist": [
|
||||
"function-url-scheme-disallowed-list": [
|
||||
"data"
|
||||
],
|
||||
"function-whitespace-after": "always",
|
||||
|
||||
@@ -7,6 +7,7 @@ const errorHandler = require('./helpers/errorHandler');
|
||||
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const HtmlWebpackPluginHtmlTags = require('html-webpack-plugin/lib/html-tags');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
const uiFolder = 'UI';
|
||||
@@ -14,7 +15,7 @@ const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = process.argv.indexOf('--production') > -1;
|
||||
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
|
||||
const inlineWebWorkers = true;
|
||||
const inlineWebWorkers = 'no-fallback';
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
@@ -32,14 +33,19 @@ const cssVarsFiles = [
|
||||
].map(require.resolve);
|
||||
|
||||
// Override the way HtmlWebpackPlugin injects the scripts
|
||||
// TODO: Find a better way to get these paths without
|
||||
HtmlWebpackPlugin.prototype.injectAssetsIntoHtml = function(html, assets, assetTags) {
|
||||
const head = assetTags.head.map((v) => {
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${v.attributes.href.replace('\\', '/')}` };
|
||||
return this.createHtmlTag(v);
|
||||
const head = assetTags.headTags.map((v) => {
|
||||
const href = v.attributes.href
|
||||
.replace('\\', '/')
|
||||
.replace('%5C', '/');
|
||||
|
||||
v.attributes = { rel: 'stylesheet', type: 'text/css', href: `/${href}` };
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
const body = assetTags.body.map((v) => {
|
||||
const body = assetTags.bodyTags.map((v) => {
|
||||
v.attributes = { src: `/${v.attributes.src}` };
|
||||
return this.createHtmlTag(v);
|
||||
return HtmlWebpackPluginHtmlTags.htmlTagObjectToString(v, this.options.xhtml);
|
||||
});
|
||||
|
||||
return html
|
||||
@@ -125,9 +131,8 @@ const config = {
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
name: '[name].js',
|
||||
inline: inlineWebWorkers,
|
||||
fallback: !inlineWebWorkers
|
||||
filename: '[name].js',
|
||||
inline: inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
@@ -10,12 +11,84 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import BlacklistRowConnector from './BlacklistRowConnector';
|
||||
|
||||
class Blacklist extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
if (hasDifferentItems(prevProps.items, items)) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = () => {
|
||||
this.props.onRemoveSelected(this.getSelectedIds());
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
onConfirmRemoveModalClose = () => {
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -27,15 +100,33 @@ class Blacklist extends Component {
|
||||
items,
|
||||
columns,
|
||||
totalRecords,
|
||||
isRemoving,
|
||||
isClearingBlacklistExecuting,
|
||||
onClearBlacklistPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen
|
||||
} = this.state;
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('Blacklist')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Remove Selected"
|
||||
iconName={icons.REMOVE}
|
||||
isDisabled={!selectedIds.length}
|
||||
isSpinning={isRemoving}
|
||||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('Clear')}
|
||||
iconName={icons.CLEAR}
|
||||
@@ -81,8 +172,12 @@ class Blacklist extends Component {
|
||||
isPopulated && !error && !!items.length &&
|
||||
<div>
|
||||
<Table
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
@@ -90,8 +185,10 @@ class Blacklist extends Component {
|
||||
return (
|
||||
<BlacklistRowConnector
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id] || false}
|
||||
columns={columns}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
@@ -107,6 +204,16 @@ class Blacklist extends Component {
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Remove Selected"
|
||||
message={'Are you sure you want to remove the selected items from the blacklist?'}
|
||||
confirmLabel="Remove Selected"
|
||||
onConfirm={this.onRemoveSelectedConfirmed}
|
||||
onCancel={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
@@ -119,7 +226,9 @@ Blacklist.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
onRemoveSelected: PropTypes.func.isRequired,
|
||||
onClearBlacklistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -89,6 +89,10 @@ class BlacklistConnector extends Component {
|
||||
this.props.gotoBlacklistPage({ page });
|
||||
}
|
||||
|
||||
onRemoveSelected = (ids) => {
|
||||
this.props.removeBlacklistItems({ ids });
|
||||
}
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setBlacklistSort({ sortKey });
|
||||
}
|
||||
@@ -124,6 +128,7 @@ class BlacklistConnector extends Component {
|
||||
onNextPagePress={this.onNextPagePress}
|
||||
onLastPagePress={this.onLastPagePress}
|
||||
onPageSelect={this.onPageSelect}
|
||||
onRemoveSelected={this.onRemoveSelected}
|
||||
onSortPress={this.onSortPress}
|
||||
onTableOptionChange={this.onTableOptionChange}
|
||||
onClearBlacklistPress={this.onClearBlacklistPress}
|
||||
@@ -143,6 +148,7 @@ BlacklistConnector.propTypes = {
|
||||
gotoBlacklistNextPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistLastPage: PropTypes.func.isRequired,
|
||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
removeBlacklistItems: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlacklist: PropTypes.func.isRequired,
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
@@ -42,6 +43,7 @@ class BlacklistRow extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
movie,
|
||||
sourceTitle,
|
||||
quality,
|
||||
@@ -51,7 +53,9 @@ class BlacklistRow extends Component {
|
||||
protocol,
|
||||
indexer,
|
||||
message,
|
||||
isSelected,
|
||||
columns,
|
||||
onSelectedChange,
|
||||
onRemovePress
|
||||
} = this.props;
|
||||
|
||||
@@ -61,6 +65,12 @@ class BlacklistRow extends Component {
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
@@ -194,7 +204,9 @@ BlacklistRow.propTypes = {
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
message: PropTypes.string,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { removeFromBlacklist } from 'Store/Actions/blacklistActions';
|
||||
import { removeBlacklistItem } from 'Store/Actions/blacklistActions';
|
||||
import createMovieSelector from 'Store/Selectors/createMovieSelector';
|
||||
import BlacklistRow from './BlacklistRow';
|
||||
|
||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRemovePress() {
|
||||
dispatch(removeFromBlacklist({ id: props.id }));
|
||||
dispatch(removeBlacklistItem({ id: props.id }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
@@ -8,10 +7,10 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return _.pick(uiSettings, [
|
||||
'shortDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
return {
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,14 +11,14 @@ function QueueDetails(props) {
|
||||
size,
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status: queueStatus,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
errorMessage,
|
||||
progressBar
|
||||
} = props;
|
||||
|
||||
const status = queueStatus.toLowerCase();
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
|
||||
if (status === 'pending') {
|
||||
return (
|
||||
@@ -40,7 +40,35 @@ function QueueDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: show an icon when download is complete, but not imported yet?
|
||||
if (trackedDownloadStatus === 'warning') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.WARNING}
|
||||
title={'Downloaded - Unable to Import: check logs for details'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={`${translate('Downloaded')} - ${translate('WaitingToImport')}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={`${translate('Downloaded')} - ${translate('Importing')}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
@@ -91,6 +119,8 @@ QueueDetails.propTypes = {
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
@@ -117,7 +117,7 @@ class AddNewMovieModalContent extends Component {
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Monitor
|
||||
{translate('Monitor')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -168,7 +168,7 @@ class AddNewMovieModalContent extends Component {
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<label className={styles.searchForMissingMovieLabelContainer}>
|
||||
<span className={styles.searchForMissingMovieLabel}>
|
||||
Start search for missing movie
|
||||
{translate('StartSearchForMissingMovie')}
|
||||
</span>
|
||||
|
||||
<CheckInput
|
||||
@@ -186,7 +186,7 @@ class AddNewMovieModalContent extends Component {
|
||||
isSpinning={isAdding}
|
||||
onPress={this.onAddMoviePress}
|
||||
>
|
||||
Add {title}
|
||||
{translate('AddMovie')}
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -170,6 +170,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
imdbId={imdbId}
|
||||
/>
|
||||
}
|
||||
canFlip={true}
|
||||
kind={kinds.INVERSE}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
|
||||
@@ -10,6 +10,7 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportMovieFooter.css';
|
||||
|
||||
const MIXED = 'mixed';
|
||||
@@ -112,7 +113,7 @@ class ImportMovieFooter extends Component {
|
||||
<PageContentFooter>
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Monitor
|
||||
{translate('Monitor')}
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -127,7 +128,7 @@ class ImportMovieFooter extends Component {
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Minimum Availability
|
||||
{translate('MinimumAvailability')}
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -142,7 +143,7 @@ class ImportMovieFooter extends Component {
|
||||
|
||||
<div className={styles.inputContainer}>
|
||||
<div className={styles.label}>
|
||||
Quality Profile
|
||||
{translate('QualityProfile')}
|
||||
</div>
|
||||
|
||||
<FormInputGroup
|
||||
@@ -168,7 +169,7 @@ class ImportMovieFooter extends Component {
|
||||
isDisabled={!selectedCount || isLookingUpMovie}
|
||||
onPress={onImportPress}
|
||||
>
|
||||
Import {selectedCount} {selectedCount > 1 ? 'Movies' : 'Movie'}
|
||||
{translate('Import')} {selectedCount} {selectedCount > 1 ? translate('Movies') : translate('Movie')}
|
||||
</SpinnerButton>
|
||||
|
||||
{
|
||||
@@ -178,7 +179,7 @@ class ImportMovieFooter extends Component {
|
||||
kind={kinds.WARNING}
|
||||
onPress={onCancelLookupPress}
|
||||
>
|
||||
Cancel Processing
|
||||
{translate('CancelProcessing')}
|
||||
</Button> :
|
||||
null
|
||||
}
|
||||
@@ -190,7 +191,7 @@ class ImportMovieFooter extends Component {
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={onLookupPress}
|
||||
>
|
||||
Start Processing
|
||||
{translate('StartProcessing')}
|
||||
</Button> :
|
||||
null
|
||||
}
|
||||
@@ -206,7 +207,7 @@ class ImportMovieFooter extends Component {
|
||||
|
||||
{
|
||||
isLookingUpMovie ?
|
||||
'Processing Folders' :
|
||||
translate('ProcessingFolders') :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -220,7 +221,7 @@ class ImportMovieFooter extends Component {
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
}
|
||||
title="Import Errors"
|
||||
title={translate('ImportErrors')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportMovieHeader.css';
|
||||
|
||||
function ImportMovieHeader(props) {
|
||||
@@ -24,35 +25,35 @@ function ImportMovieHeader(props) {
|
||||
className={styles.folder}
|
||||
name="folder"
|
||||
>
|
||||
Folder
|
||||
{translate('Folder')}
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.monitor}
|
||||
name="monitor"
|
||||
>
|
||||
Monitor
|
||||
{translate('Monitor')}
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.minimumAvailability}
|
||||
name="minimumAvailability"
|
||||
>
|
||||
Min Availability
|
||||
{translate('MinAvailability')}
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.qualityProfile}
|
||||
name="qualityProfileId"
|
||||
>
|
||||
Quality Profile
|
||||
{translate('QualityProfile')}
|
||||
</VirtualTableHeaderCell>
|
||||
|
||||
<VirtualTableHeaderCell
|
||||
className={styles.movie}
|
||||
name="movie"
|
||||
>
|
||||
Movie
|
||||
{translate('Movie')}
|
||||
</VirtualTableHeaderCell>
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Portal from 'Components/Portal';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import ImportMovieSearchResultConnector from './ImportMovieSearchResultConnector';
|
||||
import ImportMovieTitle from './ImportMovieTitle';
|
||||
import styles from './ImportMovieSelectMovie.css';
|
||||
@@ -174,7 +175,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
No match found!
|
||||
{translate('NoMatchFound')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
@@ -189,7 +190,7 @@ class ImportMovieSelectMovie extends Component {
|
||||
kind={kinds.WARNING}
|
||||
/>
|
||||
|
||||
Search failed, please try again later.
|
||||
{translate('SearchFailedPleaseTryAgainLater')}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ImportMovieTitle.css';
|
||||
|
||||
function ImportMovieTitle(props) {
|
||||
@@ -33,7 +34,7 @@ function ImportMovieTitle(props) {
|
||||
<Label
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
Existing
|
||||
{translate('Existing')}
|
||||
</Label>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -101,12 +101,8 @@ class ImportMovieSelectFolder extends Component {
|
||||
<div className={styles.tips}>
|
||||
{translate('ImportTipsMessage')}
|
||||
<ul>
|
||||
<li className={styles.tip}>
|
||||
Make sure that your files include the quality in their filenames. eg. <span className={styles.code}>movie.2008.bluray.mkv</span>
|
||||
</li>
|
||||
<li className={styles.tip}>
|
||||
Point Radarr to the folder containing all of your movies, not a specific one. eg. <span className={styles.code}>"{isWindows ? 'C:\\movies' : '/movies'}"</span> and not <span className={styles.code}>"{isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}"</span>
|
||||
</li>
|
||||
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportIncludeQuality', ['<code>movie.2008.bluray.mkv</code>']) }} />
|
||||
<li className={styles.tip} dangerouslySetInnerHTML={{ __html: translate('ImportRootPath', [`<code>${isWindows ? 'C:\\movies' : '/movies'}</code>`, `<code>${isWindows ? 'C:\\movies\\the matrix' : '/movies/the matrix'}</code>`]) }} />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -158,7 +154,7 @@ class ImportMovieSelectFolder extends Component {
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
Start Import
|
||||
{translate('StartImport')}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -6,9 +6,38 @@ import styles from './Agenda.css';
|
||||
|
||||
function Agenda(props) {
|
||||
const {
|
||||
items
|
||||
items,
|
||||
start,
|
||||
end
|
||||
} = props;
|
||||
|
||||
const startDateParsed = Date.parse(start);
|
||||
const endDateParsed = Date.parse(end);
|
||||
|
||||
items.forEach((item) => {
|
||||
const cinemaDateParsed = Date.parse(item.inCinemas);
|
||||
const digitalDateParsed = Date.parse(item.digitalRelease);
|
||||
const physicalDateParsed = Date.parse(item.physicalRelease);
|
||||
const dates = [];
|
||||
|
||||
if (cinemaDateParsed > 0 && cinemaDateParsed >= startDateParsed && cinemaDateParsed <= endDateParsed) {
|
||||
dates.push(cinemaDateParsed);
|
||||
}
|
||||
if (digitalDateParsed > 0 && digitalDateParsed >= startDateParsed && digitalDateParsed <= endDateParsed) {
|
||||
dates.push(digitalDateParsed);
|
||||
}
|
||||
if (physicalDateParsed > 0 && physicalDateParsed >= startDateParsed && physicalDateParsed <= endDateParsed) {
|
||||
dates.push(physicalDateParsed);
|
||||
}
|
||||
|
||||
item.sortDate = Math.min(...dates);
|
||||
item.cinemaDateParsed = cinemaDateParsed;
|
||||
item.digitalDateParsed = digitalDateParsed;
|
||||
item.physicalDateParsed = physicalDateParsed;
|
||||
});
|
||||
|
||||
items.sort((a, b) => ((a.sortDate > b.sortDate) ? 1 : -1));
|
||||
|
||||
return (
|
||||
<div className={styles.agenda}>
|
||||
{
|
||||
@@ -32,7 +61,9 @@ function Agenda(props) {
|
||||
}
|
||||
|
||||
Agenda.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
start: PropTypes.string.isRequired,
|
||||
end: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default Agenda;
|
||||
|
||||
@@ -92,6 +92,5 @@
|
||||
}
|
||||
|
||||
.dateIcon {
|
||||
display: inline;
|
||||
margin-right: 10px;
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
@@ -55,23 +55,26 @@ class AgendaEvent extends Component {
|
||||
showCutoffUnmetIcon,
|
||||
longDateFormat,
|
||||
colorImpairedMode,
|
||||
startDate,
|
||||
endDate
|
||||
cinemaDateParsed,
|
||||
digitalDateParsed,
|
||||
physicalDateParsed,
|
||||
sortDate
|
||||
} = this.props;
|
||||
|
||||
const agendaStart = Date.parse(startDate);
|
||||
const agendaEnd = Date.parse(endDate);
|
||||
const cinemaDate = Date.parse(inCinemas);
|
||||
const digitalDate = Date.parse(digitalRelease);
|
||||
let startTime = physicalRelease;
|
||||
let releaseIcon = icons.DISC;
|
||||
let startTime = null;
|
||||
let releaseIcon = null;
|
||||
|
||||
if (digitalDate >= agendaStart && digitalDate <= agendaEnd) {
|
||||
if (physicalDateParsed === sortDate) {
|
||||
startTime = physicalRelease;
|
||||
releaseIcon = icons.DISC;
|
||||
}
|
||||
|
||||
if (digitalDateParsed === sortDate) {
|
||||
startTime = digitalRelease;
|
||||
releaseIcon = icons.MOVIE_FILE;
|
||||
}
|
||||
|
||||
if (cinemaDate >= agendaStart && cinemaDate <= agendaEnd) {
|
||||
if (cinemaDateParsed === sortDate) {
|
||||
startTime = inCinemas;
|
||||
releaseIcon = icons.IN_CINEMAS;
|
||||
}
|
||||
@@ -92,13 +95,14 @@ class AgendaEvent extends Component {
|
||||
)}
|
||||
to={link}
|
||||
>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={releaseIcon}
|
||||
kind={kinds.DEFAULT}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.date}>
|
||||
<div className={styles.dateIcon}>
|
||||
<Icon
|
||||
name={releaseIcon}
|
||||
kind={kinds.DEFAULT}
|
||||
/>
|
||||
</div>
|
||||
{(showDate) ? startTime.format(longDateFormat) : null}
|
||||
</div>
|
||||
|
||||
@@ -176,8 +180,10 @@ AgendaEvent.propTypes = {
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
colorImpairedMode: PropTypes.bool.isRequired,
|
||||
startDate: PropTypes.date,
|
||||
endDate: PropTypes.date
|
||||
cinemaDateParsed: PropTypes.number,
|
||||
digitalDateParsed: PropTypes.number,
|
||||
physicalDateParsed: PropTypes.number,
|
||||
sortDate: PropTypes.number
|
||||
};
|
||||
|
||||
AgendaEvent.defaultProps = {
|
||||
|
||||
@@ -13,9 +13,7 @@ function createMapStateToProps() {
|
||||
createMovieFileSelector(),
|
||||
createQueueItemSelector(),
|
||||
createUISettingsSelector(),
|
||||
(state) => state.calendar.start,
|
||||
(state) => state.calendar.end,
|
||||
(calendarOptions, movie, movieFile, queueItem, uiSettings, startDate, endDate) => {
|
||||
(calendarOptions, movie, movieFile, queueItem, uiSettings) => {
|
||||
return {
|
||||
movie,
|
||||
movieFile,
|
||||
@@ -23,9 +21,7 @@ function createMapStateToProps() {
|
||||
...calendarOptions,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
startDate,
|
||||
endDate
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -12,10 +12,12 @@ function CalendarEventQueueDetails(props) {
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
|
||||
return (
|
||||
<QueueDetails
|
||||
@@ -24,6 +26,8 @@ function CalendarEventQueueDetails(props) {
|
||||
sizeleft={sizeleft}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
status={status}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
errorMessage={errorMessage}
|
||||
progressBar={
|
||||
<div title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}>
|
||||
@@ -45,6 +49,8 @@ CalendarEventQueueDetails.propTypes = {
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ function getTitle(time, start, end, view, longDateFormat) {
|
||||
} else if (view === 'month') {
|
||||
return timeMoment.format('MMMM YYYY');
|
||||
} else if (view === 'agenda') {
|
||||
return 'Agenda';
|
||||
return `Agenda: ${startMoment.format('MMM D')} - ${endMoment.format('MMM D')}`;
|
||||
}
|
||||
|
||||
let startFormat = 'MMM D YYYY';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -14,19 +13,16 @@ function createMapStateToProps() {
|
||||
createDimensionsSelector(),
|
||||
createUISettingsSelector(),
|
||||
(calendar, dimensions, uiSettings) => {
|
||||
const result = _.pick(calendar, [
|
||||
'isFetching',
|
||||
'view',
|
||||
'time',
|
||||
'start',
|
||||
'end'
|
||||
]);
|
||||
|
||||
result.isSmallScreen = dimensions.isSmallScreen;
|
||||
result.collapseViewButtons = dimensions.isLargeScreen;
|
||||
result.longDateFormat = uiSettings.longDateFormat;
|
||||
|
||||
return result;
|
||||
return {
|
||||
isFetching: calendar.isFetching,
|
||||
view: calendar.view,
|
||||
time: calendar.time,
|
||||
start: calendar.start,
|
||||
end: calendar.end,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
collapseViewButtons: dimensions.isLargeScreen,
|
||||
longDateFormat: uiSettings.longDateFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,11 +58,30 @@ function getSelectedIndex(props) {
|
||||
values
|
||||
} = props;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return values.findIndex((v) => {
|
||||
return value.size && v.key === value[0];
|
||||
});
|
||||
}
|
||||
|
||||
return values.findIndex((v) => {
|
||||
return v.key === value;
|
||||
});
|
||||
}
|
||||
|
||||
function isSelectedItem(index, props) {
|
||||
const {
|
||||
value,
|
||||
values
|
||||
} = props;
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.includes(values[index].key);
|
||||
}
|
||||
|
||||
return values[index].key === value;
|
||||
}
|
||||
|
||||
function getKey(selectedIndex, values) {
|
||||
return values[selectedIndex].key;
|
||||
}
|
||||
@@ -92,7 +111,7 @@ class EnhancedSelectInput extends Component {
|
||||
this._scheduleUpdate();
|
||||
}
|
||||
|
||||
if (prevProps.value !== this.props.value) {
|
||||
if (!Array.isArray(this.props.value) && prevProps.value !== this.props.value) {
|
||||
this.setState({
|
||||
selectedIndex: getSelectedIndex(this.props)
|
||||
});
|
||||
@@ -134,7 +153,7 @@ class EnhancedSelectInput extends Component {
|
||||
const button = document.getElementById(this._buttonId);
|
||||
const options = document.getElementById(this._optionsId);
|
||||
|
||||
if (!button || this.state.isMobile) {
|
||||
if (!button || !event.target.isConnected || this.state.isMobile) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -177,7 +196,7 @@ class EnhancedSelectInput extends Component {
|
||||
}
|
||||
|
||||
if (
|
||||
selectedIndex == null ||
|
||||
selectedIndex == null || selectedIndex === -1 ||
|
||||
getSelectedOption(selectedIndex, values).isDisabled
|
||||
) {
|
||||
if (keyCode === keyCodes.UP_ARROW) {
|
||||
@@ -235,12 +254,27 @@ class EnhancedSelectInput extends Component {
|
||||
}
|
||||
|
||||
onSelect = (value) => {
|
||||
this.setState({ isOpen: false });
|
||||
if (Array.isArray(this.props.value)) {
|
||||
let newValue = null;
|
||||
const index = this.props.value.indexOf(value);
|
||||
if (index === -1) {
|
||||
newValue = this.props.values.map((v) => v.key).filter((v) => (v === value) || this.props.value.includes(v));
|
||||
} else {
|
||||
newValue = [...this.props.value];
|
||||
newValue.splice(index, 1);
|
||||
}
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value: newValue
|
||||
});
|
||||
} else {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
});
|
||||
this.props.onChange({
|
||||
name: this.props.name,
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMeasure = ({ width }) => {
|
||||
@@ -258,6 +292,7 @@ class EnhancedSelectInput extends Component {
|
||||
const {
|
||||
className,
|
||||
disabledClassName,
|
||||
value,
|
||||
values,
|
||||
isDisabled,
|
||||
hasError,
|
||||
@@ -275,6 +310,7 @@ class EnhancedSelectInput extends Component {
|
||||
isMobile
|
||||
} = this.state;
|
||||
|
||||
const isMultiSelect = Array.isArray(value);
|
||||
const selectedOption = getSelectedOption(selectedIndex, values);
|
||||
|
||||
return (
|
||||
@@ -303,9 +339,12 @@ class EnhancedSelectInput extends Component {
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
value={value}
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
@@ -359,11 +398,17 @@ class EnhancedSelectInput extends Component {
|
||||
>
|
||||
{
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
isSelected={index === selectedIndex}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, this.props)}
|
||||
isDisabled={parentSelected}
|
||||
isMultiSelect={isMultiSelect}
|
||||
{...valueOptions}
|
||||
{...v}
|
||||
isMobile={false}
|
||||
@@ -401,11 +446,17 @@ class EnhancedSelectInput extends Component {
|
||||
<Scroller className={styles.optionsModalScroller}>
|
||||
{
|
||||
values.map((v, index) => {
|
||||
const hasParent = v.parentKey !== undefined;
|
||||
const depth = hasParent ? 1 : 0;
|
||||
const parentSelected = hasParent && value.includes(v.parentKey);
|
||||
return (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
isSelected={index === selectedIndex}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, this.props)}
|
||||
isMultiSelect={isMultiSelect}
|
||||
isDisabled={parentSelected}
|
||||
{...valueOptions}
|
||||
{...v}
|
||||
isMobile={true}
|
||||
@@ -429,9 +480,9 @@ EnhancedSelectInput.propTypes = {
|
||||
className: PropTypes.string,
|
||||
disabledClassName: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isDisabled: PropTypes.bool,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
valueOptions: PropTypes.object.isRequired,
|
||||
|
||||
@@ -11,6 +11,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.optionCheck {
|
||||
composes: container from '~./CheckInput.css';
|
||||
|
||||
flex: 0 0 0;
|
||||
}
|
||||
|
||||
.optionCheckInput {
|
||||
composes: input from '~./CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.isSelected {
|
||||
background-color: #e2e2e2;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CheckInput from './CheckInput';
|
||||
import styles from './EnhancedSelectInputOption.css';
|
||||
|
||||
class EnhancedSelectInputOption extends Component {
|
||||
@@ -20,15 +21,21 @@ class EnhancedSelectInputOption extends Component {
|
||||
onSelect(id);
|
||||
}
|
||||
|
||||
onCheckPress = () => {
|
||||
// CheckInput requires a handler. Swallow the change event because onPress will already handle it via event propagation.
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
id,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isHidden,
|
||||
isMultiSelect,
|
||||
isMobile,
|
||||
children
|
||||
} = this.props;
|
||||
@@ -37,8 +44,8 @@ class EnhancedSelectInputOption extends Component {
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
isSelected && styles.isSelected,
|
||||
isDisabled && styles.isDisabled,
|
||||
isSelected && !isMultiSelect && styles.isSelected,
|
||||
isDisabled && !isMultiSelect && styles.isDisabled,
|
||||
isHidden && styles.isHidden,
|
||||
isMobile && styles.isMobile
|
||||
)}
|
||||
@@ -46,6 +53,19 @@ class EnhancedSelectInputOption extends Component {
|
||||
isDisabled={isDisabled}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
|
||||
{
|
||||
isMultiSelect &&
|
||||
<CheckInput
|
||||
className={styles.optionCheckInput}
|
||||
containerClassName={styles.optionCheck}
|
||||
name={`select-${id}`}
|
||||
value={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
onChange={this.onCheckPress}
|
||||
/>
|
||||
}
|
||||
|
||||
{children}
|
||||
|
||||
{
|
||||
@@ -67,6 +87,7 @@ EnhancedSelectInputOption.propTypes = {
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
isMobile: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onSelect: PropTypes.func.isRequired
|
||||
@@ -75,7 +96,8 @@ EnhancedSelectInputOption.propTypes = {
|
||||
EnhancedSelectInputOption.defaultProps = {
|
||||
className: styles.option,
|
||||
isDisabled: false,
|
||||
isHidden: false
|
||||
isHidden: false,
|
||||
isMultiSelect: false
|
||||
};
|
||||
|
||||
export default EnhancedSelectInputOption;
|
||||
|
||||
@@ -10,6 +10,7 @@ import CheckInput from './CheckInput';
|
||||
import DeviceInputConnector from './DeviceInputConnector';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
import FormInputHelpText from './FormInputHelpText';
|
||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
||||
import KeyValueListInput from './KeyValueListInput';
|
||||
import MovieMonitoredSelectInput from './MovieMonitoredSelectInput';
|
||||
import NumberInput from './NumberInput';
|
||||
@@ -66,6 +67,9 @@ function getComponent(type) {
|
||||
case inputTypes.ROOT_FOLDER_SELECT:
|
||||
return RootFolderSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||
return IndexerFlagsSelectInputConnector;
|
||||
|
||||
case inputTypes.SELECT:
|
||||
return EnhancedSelectInput;
|
||||
|
||||
|
||||
@@ -6,14 +6,23 @@ import styles from './HintedSelectInputOption.css';
|
||||
|
||||
function HintedSelectInputOption(props) {
|
||||
const {
|
||||
id,
|
||||
value,
|
||||
hint,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isMultiSelect,
|
||||
isMobile,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputOption
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isHidden={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
isMobile={isMobile}
|
||||
{...otherProps}
|
||||
>
|
||||
@@ -36,9 +45,19 @@ function HintedSelectInputOption(props) {
|
||||
}
|
||||
|
||||
HintedSelectInputOption.propTypes = {
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
hint: PropTypes.node,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
isMobile: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
HintedSelectInputOption.defaultProps = {
|
||||
isDisabled: false,
|
||||
isHidden: false,
|
||||
isMultiSelect: false
|
||||
};
|
||||
|
||||
export default HintedSelectInputOption;
|
||||
|
||||
@@ -1,23 +1,43 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import EnhancedSelectInputSelectedValue from './EnhancedSelectInputSelectedValue';
|
||||
import styles from './HintedSelectInputSelectedValue.css';
|
||||
|
||||
function HintedSelectInputSelectedValue(props) {
|
||||
const {
|
||||
value,
|
||||
values,
|
||||
hint,
|
||||
isMultiSelect,
|
||||
includeHint,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
const valuesMap = isMultiSelect && _.keyBy(values, 'key');
|
||||
|
||||
return (
|
||||
<EnhancedSelectInputSelectedValue
|
||||
className={styles.selectedValue}
|
||||
{...otherProps}
|
||||
>
|
||||
<div className={styles.valueText}>
|
||||
{value}
|
||||
{
|
||||
isMultiSelect &&
|
||||
value.map((key, index) => {
|
||||
const v = valuesMap[key];
|
||||
return (
|
||||
<Label key={key}>
|
||||
{v ? v.value : key}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
!isMultiSelect && value
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
@@ -31,12 +51,15 @@ function HintedSelectInputSelectedValue(props) {
|
||||
}
|
||||
|
||||
HintedSelectInputSelectedValue.propTypes = {
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number]))]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hint: PropTypes.string,
|
||||
isMultiSelect: PropTypes.bool.isRequired,
|
||||
includeHint: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
HintedSelectInputSelectedValue.defaultProps = {
|
||||
isMultiSelect: false,
|
||||
includeHint: true
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { indexerFlags }) => indexerFlags,
|
||||
(state) => state.settings.indexerFlags,
|
||||
(selectedFlags, indexerFlags) => {
|
||||
const value = [];
|
||||
|
||||
indexerFlags.items.forEach((item) => {
|
||||
// eslint-disable-next-line no-bitwise
|
||||
if ((selectedFlags & item.id) === item.id) {
|
||||
value.push(item.id);
|
||||
}
|
||||
});
|
||||
|
||||
const values = indexerFlags.items.map(({ id, name }) => {
|
||||
return {
|
||||
key: id,
|
||||
value: name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class IndexerFlagsSelectInputConnector extends Component {
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
let indexerFlags = 0;
|
||||
|
||||
value.forEach((flagId) => {
|
||||
indexerFlags += flagId;
|
||||
});
|
||||
|
||||
this.props.onChange({ name, value: indexerFlags });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerFlagsSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(IndexerFlagsSelectInputConnector);
|
||||
@@ -3,10 +3,17 @@ import React from 'react';
|
||||
import TextInput from './TextInput';
|
||||
import styles from './PasswordInput.css';
|
||||
|
||||
// Prevent a user from copying (or cutting) the password from the input
|
||||
function onCopy(e) {
|
||||
e.preventDefault();
|
||||
e.nativeEvent.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
function PasswordInput(props) {
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
onCopy={onCopy}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
|
||||
function getType(type) {
|
||||
function getType(type, value) {
|
||||
switch (type) {
|
||||
case 'captcha':
|
||||
return inputTypes.CAPTCHA;
|
||||
@@ -45,7 +45,8 @@ function getSelectValues(selectOptions) {
|
||||
return _.reduce(selectOptions, (result, option) => {
|
||||
result.push({
|
||||
key: option.value,
|
||||
value: option.name
|
||||
value: option.name,
|
||||
hint: option.hint
|
||||
});
|
||||
|
||||
return result;
|
||||
@@ -87,7 +88,7 @@ function ProviderFieldFormGroup(props) {
|
||||
<FormLabel>{label}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={getType(type)}
|
||||
type={getType(type, value)}
|
||||
name={name}
|
||||
label={label}
|
||||
helpText={helpText}
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.movieFolder {
|
||||
flex: 0 0 auto;
|
||||
@add-mixin truncate;
|
||||
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,8 @@ class TextInput extends Component {
|
||||
step,
|
||||
min,
|
||||
max,
|
||||
onBlur
|
||||
onBlur,
|
||||
onCopy
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
@@ -155,6 +156,8 @@ class TextInput extends Component {
|
||||
onChange={this.onChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={onBlur}
|
||||
onCopy={onCopy}
|
||||
onCut={onCopy}
|
||||
onKeyUp={this.onKeyUp}
|
||||
onMouseDown={this.onMouseDown}
|
||||
onMouseUp={this.onMouseUp}
|
||||
@@ -180,6 +183,7 @@ TextInput.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onFocus: PropTypes.func,
|
||||
onBlur: PropTypes.func,
|
||||
onCopy: PropTypes.func,
|
||||
onSelectionChange: PropTypes.func
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,24 @@ import React from 'react';
|
||||
import styles from './LoadingMessage.css';
|
||||
|
||||
const messages = [
|
||||
'Welcome to Radarr Aphrodite Preview. Enjoy'
|
||||
// TODO Add some messages here
|
||||
'Downloading more RAM',
|
||||
'Now in Technicolor',
|
||||
'Previously on Radarr...',
|
||||
'Bleep Bloop.',
|
||||
'Locating the required gigapixels to render...',
|
||||
'Spinning up the hamster wheel...',
|
||||
'At least you\'re not on hold',
|
||||
'Hum something loud while others stare',
|
||||
'Loading humorous message... Please Wait',
|
||||
'I could\'ve been faster in Python',
|
||||
'Don\'t forget to rewind your tracks',
|
||||
'Congratulations! you are the 1000th visitor.',
|
||||
'HELP! I\'m being held hostage and forced to write these stupid lines!',
|
||||
'RE-calibrating the internet...',
|
||||
'I\'ll be here all week',
|
||||
'Don\'t forget to tip your waitress',
|
||||
'Apply directly to the forehead',
|
||||
'Loading Battlestation'
|
||||
];
|
||||
|
||||
let message = null;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createSelector } from 'reselect';
|
||||
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
|
||||
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
|
||||
import { fetchMovies } from 'Store/Actions/movieActions';
|
||||
import { fetchImportLists, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchImportLists, fetchIndexerFlags, fetchLanguages, fetchQualityProfiles, fetchUISettings } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import { fetchTags } from 'Store/Actions/tagActions';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
@@ -48,6 +48,7 @@ const selectIsPopulated = createSelector(
|
||||
(state) => state.settings.ui.isPopulated,
|
||||
(state) => state.settings.qualityProfiles.isPopulated,
|
||||
(state) => state.settings.languages.isPopulated,
|
||||
(state) => state.settings.indexerFlags.isPopulated,
|
||||
(state) => state.settings.importLists.isPopulated,
|
||||
(state) => state.system.status.isPopulated,
|
||||
(
|
||||
@@ -56,6 +57,7 @@ const selectIsPopulated = createSelector(
|
||||
uiSettingsIsPopulated,
|
||||
qualityProfilesIsPopulated,
|
||||
languagesIsPopulated,
|
||||
indexerFlagsIsPopulated,
|
||||
importListsIsPopulated,
|
||||
systemStatusIsPopulated
|
||||
) => {
|
||||
@@ -65,6 +67,7 @@ const selectIsPopulated = createSelector(
|
||||
uiSettingsIsPopulated &&
|
||||
qualityProfilesIsPopulated &&
|
||||
languagesIsPopulated &&
|
||||
indexerFlagsIsPopulated &&
|
||||
importListsIsPopulated &&
|
||||
systemStatusIsPopulated
|
||||
);
|
||||
@@ -77,6 +80,7 @@ const selectErrors = createSelector(
|
||||
(state) => state.settings.ui.error,
|
||||
(state) => state.settings.qualityProfiles.error,
|
||||
(state) => state.settings.languages.error,
|
||||
(state) => state.settings.indexerFlags.error,
|
||||
(state) => state.settings.importLists.error,
|
||||
(state) => state.system.status.error,
|
||||
(
|
||||
@@ -85,6 +89,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
indexerFlagsError,
|
||||
importListsError,
|
||||
systemStatusError
|
||||
) => {
|
||||
@@ -94,6 +99,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError ||
|
||||
qualityProfilesError ||
|
||||
languagesError ||
|
||||
indexerFlagsError ||
|
||||
importListsError ||
|
||||
systemStatusError
|
||||
);
|
||||
@@ -105,6 +111,7 @@ const selectErrors = createSelector(
|
||||
uiSettingsError,
|
||||
qualityProfilesError,
|
||||
languagesError,
|
||||
indexerFlagsError,
|
||||
importListsError,
|
||||
systemStatusError
|
||||
};
|
||||
@@ -153,6 +160,9 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatchFetchLanguages() {
|
||||
dispatch(fetchLanguages());
|
||||
},
|
||||
dispatchFetchIndexerFlags() {
|
||||
dispatch(fetchIndexerFlags());
|
||||
},
|
||||
dispatchFetchImportLists() {
|
||||
dispatch(fetchImportLists());
|
||||
},
|
||||
@@ -191,6 +201,7 @@ class PageConnector extends Component {
|
||||
this.props.dispatchFetchTags();
|
||||
this.props.dispatchFetchQualityProfiles();
|
||||
this.props.dispatchFetchLanguages();
|
||||
this.props.dispatchFetchIndexerFlags();
|
||||
this.props.dispatchFetchImportLists();
|
||||
this.props.dispatchFetchUISettings();
|
||||
this.props.dispatchFetchStatus();
|
||||
@@ -215,6 +226,7 @@ class PageConnector extends Component {
|
||||
dispatchFetchTags,
|
||||
dispatchFetchQualityProfiles,
|
||||
dispatchFetchLanguages,
|
||||
dispatchFetchIndexerFlags,
|
||||
dispatchFetchImportLists,
|
||||
dispatchFetchUISettings,
|
||||
dispatchFetchStatus,
|
||||
@@ -254,6 +266,7 @@ PageConnector.propTypes = {
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchQualityProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchLanguages: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexerFlags: PropTypes.func.isRequired,
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
dispatchFetchUISettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
@@ -8,12 +7,12 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return _.pick(uiSettings, [
|
||||
'showRelativeDates',
|
||||
'shortDateFormat',
|
||||
'longDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import Form from 'Components/Form/Form';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
@@ -156,3 +156,35 @@
|
||||
.body {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.verticalContainer {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointExtraSmall - 20px);
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointExtraSmall) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointSmall * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointSmall) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointMedium * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width: $breakpointMedium) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointLarge * 0.8);
|
||||
}
|
||||
}
|
||||
|
||||
/* @media only screen and (max-width: $breakpointLarge) {
|
||||
.horizontalContainer {
|
||||
max-width: calc($breakpointLarge * 0.8);
|
||||
}
|
||||
} */
|
||||
|
||||
@@ -4,9 +4,26 @@ import React, { Component } from 'react';
|
||||
import { Manager, Popper, Reference } from 'react-popper';
|
||||
import Portal from 'Components/Portal';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import styles from './Tooltip.css';
|
||||
|
||||
let maxWidth = null;
|
||||
|
||||
function getMaxWidth() {
|
||||
const windowWidth = window.innerWidth;
|
||||
|
||||
if (windowWidth >= parseInt(dimensions.breakpointLarge)) {
|
||||
maxWidth = 800;
|
||||
} else if (windowWidth >= parseInt(dimensions.breakpointMedium)) {
|
||||
maxWidth = 650;
|
||||
} else if (windowWidth >= parseInt(dimensions.breakpointSmall)) {
|
||||
maxWidth = 500;
|
||||
} else {
|
||||
maxWidth = 450;
|
||||
}
|
||||
}
|
||||
|
||||
class Tooltip extends Component {
|
||||
|
||||
//
|
||||
@@ -17,6 +34,7 @@ class Tooltip extends Component {
|
||||
|
||||
this._scheduleUpdate = null;
|
||||
this._closeTimeout = null;
|
||||
this._maxWidth = maxWidth || getMaxWidth();
|
||||
|
||||
this.state = {
|
||||
isOpen: false
|
||||
@@ -54,9 +72,11 @@ class Tooltip extends Component {
|
||||
} else if ((/^bottom/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom - 20;
|
||||
} else if ((/^right/).test(data.placement)) {
|
||||
data.styles.maxWidth = windowWidth - right - 50;
|
||||
data.styles.maxWidth = Math.min(this._maxWidth, windowWidth - right - 20);
|
||||
data.styles.maxHeight = top - 20;
|
||||
} else {
|
||||
data.styles.maxWidth = left - 35;
|
||||
data.styles.maxWidth = Math.min(this._maxWidth, left - 20);
|
||||
data.styles.maxHeight = top - 20;
|
||||
}
|
||||
|
||||
return data;
|
||||
@@ -144,10 +164,16 @@ class Tooltip extends Component {
|
||||
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
||||
this._scheduleUpdate = scheduleUpdate;
|
||||
|
||||
const popperPlacement = placement ? placement.split('-')[0] : position;
|
||||
const vertical = popperPlacement === 'top' || popperPlacement === 'bottom';
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={styles.tooltipContainer}
|
||||
className={classNames(
|
||||
styles.tooltipContainer,
|
||||
vertical ? styles.verticalContainer : styles.horizontalContainer
|
||||
)}
|
||||
style={style}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
@@ -156,7 +182,7 @@ class Tooltip extends Component {
|
||||
className={this.state.isOpen ? classNames(
|
||||
styles.arrow,
|
||||
styles[kind],
|
||||
styles[placement.split('-')[0]]
|
||||
styles[popperPlacement]
|
||||
) : styles.arrowDisabled}
|
||||
ref={arrowProps.ref}
|
||||
style={arrowProps.style}
|
||||
@@ -201,7 +227,7 @@ Tooltip.defaultProps = {
|
||||
bodyClassName: styles.body,
|
||||
kind: kinds.DEFAULT,
|
||||
position: tooltipPositions.TOP,
|
||||
canFlip: true
|
||||
canFlip: false
|
||||
};
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
@@ -10,6 +10,7 @@ export const PASSWORD = 'password';
|
||||
export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const INDEXER_FLAGS_SELECT = 'indexerFlagsSelect';
|
||||
export const SELECT = 'select';
|
||||
export const TAG = 'tag';
|
||||
export const TEXT = 'text';
|
||||
@@ -30,6 +31,7 @@ export const all = [
|
||||
PATH,
|
||||
QUALITY_PROFILE_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
INDEXER_FLAGS_SELECT,
|
||||
SELECT,
|
||||
TAG,
|
||||
TEXT,
|
||||
|
||||
@@ -18,11 +18,14 @@ function createMapStateToProps() {
|
||||
items
|
||||
} = languages;
|
||||
|
||||
const filterItems = ['Any', 'Original'];
|
||||
const filteredLanguages = items.filter((lang) => !filterItems.includes(lang.name));
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
items: filteredLanguages
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -54,7 +57,9 @@ class SelectLanguageModalContentConnector extends Component {
|
||||
const language = _.find(this.props.items,
|
||||
(item) => item.id === parseInt(languageId));
|
||||
|
||||
languages.push(language);
|
||||
if (language !== undefined) {
|
||||
languages.push(language);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.dispatchUpdateInteractiveImportItems({
|
||||
|
||||
@@ -8,8 +8,8 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { guid }) => guid,
|
||||
(state) => state.movieHistory.items,
|
||||
(state) => state.blacklist.items,
|
||||
(guid, movieHistory, blacklist) => {
|
||||
(state) => state.movieBlacklist.items,
|
||||
(guid, movieHistory, movieBlacklist) => {
|
||||
|
||||
let blacklistData = {};
|
||||
let historyFailedData = {};
|
||||
@@ -17,7 +17,7 @@ function createMapStateToProps() {
|
||||
const historyGrabbedData = movieHistory.find((movie) => movie.eventType === 'grabbed' && movie.data.guid === guid);
|
||||
if (historyGrabbedData) {
|
||||
historyFailedData = movieHistory.find((movie) => movie.eventType === 'downloadFailed' && movie.sourceTitle === historyGrabbedData.sourceTitle);
|
||||
blacklistData = blacklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
|
||||
blacklistData = movieBlacklist.find((item) => item.sourceTitle === historyGrabbedData.sourceTitle);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -35,8 +35,8 @@ class DeleteMovieModalContentConnector extends Component {
|
||||
|
||||
this.props.onModalClose(true);
|
||||
|
||||
if (this.props.previousMovie) {
|
||||
this.props.push(this.props.previousMovie);
|
||||
if (this.props.nextMovieRelativePath) {
|
||||
this.props.push(window.Radarr.urlBase + this.props.nextMovieRelativePath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ DeleteMovieModalContentConnector.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
deleteMovie: PropTypes.func.isRequired,
|
||||
push: PropTypes.func.isRequired,
|
||||
previousMovie: PropTypes.string
|
||||
nextMovieRelativePath: PropTypes.string
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteMovieModalContentConnector);
|
||||
|
||||
@@ -17,6 +17,7 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import InteractiveSearchFilterMenuConnector from 'InteractiveSearch/InteractiveSearchFilterMenuConnector';
|
||||
@@ -100,6 +101,7 @@ class MovieDetails extends Component {
|
||||
window.removeEventListener('touchend', this.onTouchEnd);
|
||||
window.removeEventListener('touchcancel', this.onTouchCancel);
|
||||
window.removeEventListener('touchmove', this.onTouchMove);
|
||||
window.removeEventListener('keyup', this.onKeyUp);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -176,11 +178,13 @@ class MovieDetails extends Component {
|
||||
}
|
||||
|
||||
onKeyUp = (event) => {
|
||||
if (event.keyCode === keyCodes.LEFT_ARROW) {
|
||||
this.props.onGoToMovie(this.props.previousMovie.titleSlug);
|
||||
}
|
||||
if (event.keyCode === keyCodes.RIGHT_ARROW) {
|
||||
this.props.onGoToMovie(this.props.nextMovie.titleSlug);
|
||||
if (event.path.length === 4) {
|
||||
if (event.keyCode === keyCodes.LEFT_ARROW) {
|
||||
this.props.onGoToMovie(this.props.previousMovie.titleSlug);
|
||||
}
|
||||
if (event.keyCode === keyCodes.RIGHT_ARROW) {
|
||||
this.props.onGoToMovie(this.props.nextMovie.titleSlug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +240,10 @@ class MovieDetails extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
onTabSelect = (index, lastIndex) => {
|
||||
this.setState({ selectedTabIndex: index });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -454,15 +462,14 @@ class MovieDetails extends Component {
|
||||
|
||||
{
|
||||
<span className={styles.links}>
|
||||
<Popover
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
tooltip={
|
||||
<MovieDetailsLinks
|
||||
tmdbId={tmdbId}
|
||||
imdbId={imdbId}
|
||||
@@ -477,15 +484,14 @@ class MovieDetails extends Component {
|
||||
{
|
||||
!!tags.length &&
|
||||
<span>
|
||||
<Popover
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.TAGS}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
title={translate('Tags')}
|
||||
body={
|
||||
tooltip={
|
||||
<MovieTagsConnector movieId={id} />
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
@@ -543,7 +549,7 @@ class MovieDetails extends Component {
|
||||
>
|
||||
<span className={styles.sizeOnDisk}>
|
||||
{
|
||||
formatBytes(sizeOnDisk)
|
||||
formatBytes(sizeOnDisk || 0)
|
||||
}
|
||||
</span>
|
||||
</InfoLabel>
|
||||
@@ -613,7 +619,7 @@ class MovieDetails extends Component {
|
||||
</div>
|
||||
}
|
||||
|
||||
<Tabs selectedIndex={this.state.tabIndex} onSelect={(tabIndex) => this.setState({ selectedTabIndex: tabIndex })}>
|
||||
<Tabs selectedIndex={this.state.tabIndex} onSelect={this.onTabSelect}>
|
||||
<TabList
|
||||
className={styles.tabList}
|
||||
>
|
||||
@@ -727,7 +733,7 @@ class MovieDetails extends Component {
|
||||
isOpen={isDeleteMovieModalOpen}
|
||||
movieId={id}
|
||||
onModalClose={this.onDeleteMovieModalClose}
|
||||
previousMovie={`/movie/${previousMovie.titleSlug}`}
|
||||
nextMovieRelativePath={`/movie/${nextMovie.titleSlug}`}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
|
||||
@@ -5,10 +5,10 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { fetchBlacklist } from 'Store/Actions/blacklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { clearExtraFiles, fetchExtraFiles } from 'Store/Actions/extraFileActions';
|
||||
import { toggleMovieMonitored } from 'Store/Actions/movieActions';
|
||||
import { clearMovieBlacklist, fetchMovieBlacklist } from 'Store/Actions/movieBlacklistActions';
|
||||
import { clearMovieCredits, fetchMovieCredits } from 'Store/Actions/movieCreditsActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearMovieHistory, fetchMovieHistory } from 'Store/Actions/movieHistoryActions';
|
||||
@@ -222,9 +222,11 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
onGoToMovie(titleSlug) {
|
||||
dispatch(push(`${window.Radarr.urlBase}/movie/${titleSlug}`));
|
||||
},
|
||||
dispatchFetchBlacklist() {
|
||||
// TODO: Allow for passing a movie id to fetch a single movie's blacklist data
|
||||
dispatch(fetchBlacklist());
|
||||
dispatchFetchMovieBlacklist({ movieId }) {
|
||||
dispatch(fetchMovieBlacklist({ movieId }));
|
||||
},
|
||||
dispatchClearMovieBlacklist() {
|
||||
dispatch(clearMovieBlacklist());
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -278,20 +280,17 @@ class MovieDetailsConnector extends Component {
|
||||
const movieId = this.props.id;
|
||||
|
||||
this.props.dispatchFetchMovieFiles({ movieId });
|
||||
this.props.dispatchFetchMovieBlacklist({ movieId });
|
||||
this.props.dispatchFetchMovieHistory({ movieId });
|
||||
this.props.dispatchFetchExtraFiles({ movieId });
|
||||
this.props.dispatchFetchMovieCredits({ movieId });
|
||||
this.props.dispatchFetchQueueDetails({ movieId });
|
||||
this.props.dispatchFetchImportListSchema();
|
||||
this.props.dispatchFetchBlacklist();
|
||||
}
|
||||
|
||||
repopulate = () => {
|
||||
this.props.dispatchFetchBlacklist();
|
||||
}
|
||||
|
||||
unpopulate = () => {
|
||||
this.props.dispatchCancelFetchReleases();
|
||||
this.props.dispatchClearMovieBlacklist();
|
||||
this.props.dispatchClearMovieFiles();
|
||||
this.props.dispatchClearMovieHistory();
|
||||
this.props.dispatchClearExtraFiles();
|
||||
@@ -363,7 +362,8 @@ MovieDetailsConnector.propTypes = {
|
||||
dispatchClearQueueDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchImportListSchema: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired,
|
||||
dispatchFetchBlacklist: PropTypes.func.isRequired,
|
||||
dispatchFetchMovieBlacklist: PropTypes.func.isRequired,
|
||||
dispatchClearMovieBlacklist: PropTypes.func.isRequired,
|
||||
onGoToMovie: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
@@ -8,12 +7,12 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createUISettingsSelector(),
|
||||
(uiSettings) => {
|
||||
return _.pick(uiSettings, [
|
||||
'showRelativeDates',
|
||||
'shortDateFormat',
|
||||
'longDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
return {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -36,13 +35,13 @@ function createMapStateToProps() {
|
||||
pendingChanges
|
||||
} = moviesState;
|
||||
|
||||
const movieSettings = _.pick(movie, [
|
||||
'monitored',
|
||||
'qualityProfileId',
|
||||
'minimumAvailability',
|
||||
'path',
|
||||
'tags'
|
||||
]);
|
||||
const movieSettings = {
|
||||
monitored: movie.monitored,
|
||||
qualityProfileId: movie.qualityProfileId,
|
||||
minimumAvailability: movie.minimumAvailability,
|
||||
path: movie.path,
|
||||
tags: movie.tags
|
||||
};
|
||||
|
||||
const settings = selectSettings(movieSettings, pendingChanges, saveError);
|
||||
|
||||
|
||||
@@ -72,7 +72,8 @@ class MovieIndexOverviews extends Component {
|
||||
sortKey,
|
||||
overviewOptions,
|
||||
jumpToCharacter,
|
||||
isMovieEditorActive
|
||||
isMovieEditorActive,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -82,13 +83,15 @@ class MovieIndexOverviews extends Component {
|
||||
|
||||
if (prevProps.sortKey !== sortKey ||
|
||||
prevProps.overviewOptions !== overviewOptions) {
|
||||
this.calculateGrid();
|
||||
this.calculateGrid(this.state.width, isSmallScreen);
|
||||
}
|
||||
|
||||
if (this._grid &&
|
||||
if (
|
||||
this._grid &&
|
||||
(prevState.width !== width ||
|
||||
prevState.rowHeight !== rowHeight ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items) ||
|
||||
prevProps.overviewOptions !== overviewOptions ||
|
||||
prevProps.isMovieEditorActive !== isMovieEditorActive)) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './MovieIndexPosterInfo.css';
|
||||
|
||||
function MovieIndexPosterInfo(props) {
|
||||
@@ -51,7 +52,7 @@ function MovieIndexPosterInfo(props) {
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{`Added ${addedDate}`}
|
||||
{translate('Added')}: {addedDate}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -69,7 +70,7 @@ function MovieIndexPosterInfo(props) {
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{`In Cinemas ${inCinemasDate}`}
|
||||
{translate('InCinemas')}: {inCinemasDate}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -87,7 +88,7 @@ function MovieIndexPosterInfo(props) {
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{`Digital ${digitalReleaseDate}`}
|
||||
{translate('Digital')}: {digitalReleaseDate}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -105,7 +106,7 @@ function MovieIndexPosterInfo(props) {
|
||||
|
||||
return (
|
||||
<div className={styles.info}>
|
||||
{`Released ${physicalReleaseDate}`}
|
||||
{translate('Released')}: {physicalReleaseDate}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import TagListConnector from 'Components/TagListConnector';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
@@ -369,21 +369,22 @@ class MovieIndexRow extends Component {
|
||||
className={styles[name]}
|
||||
>
|
||||
<span className={styles.externalLinks}>
|
||||
<Popover
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.EXTERNAL_LINK}
|
||||
size={12}
|
||||
/>
|
||||
}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
tooltip={
|
||||
<MovieDetailsLinks
|
||||
tmdbId={tmdbId}
|
||||
imdbId={imdbId}
|
||||
youTubeTrailerId={youTubeTrailerId}
|
||||
/>
|
||||
}
|
||||
canFlip={true}
|
||||
kind={kinds.INVERSE}
|
||||
/>
|
||||
</span>
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -10,16 +9,13 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieSelector(),
|
||||
(movie) => {
|
||||
const result = _.pick(movie, [
|
||||
'inCinemas',
|
||||
'isAvailable',
|
||||
'monitored',
|
||||
'grabbed'
|
||||
]);
|
||||
|
||||
result.movieFile = movie.movieFile;
|
||||
|
||||
return result;
|
||||
return {
|
||||
inCinemas: movie.inCinemas,
|
||||
isAvailable: movie.isAvailable,
|
||||
monitored: movie.monitored,
|
||||
grabbed: movie.grabbed,
|
||||
movieFile: movie.movieFile
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
37
frontend/src/MovieFile/Edit/FileEditModal.js
Normal file
37
frontend/src/MovieFile/Edit/FileEditModal.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import FileEditModalContentConnector from './FileEditModalContentConnector';
|
||||
|
||||
class FileEditModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<FileEditModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileEditModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default FileEditModal;
|
||||
237
frontend/src/MovieFile/Edit/FileEditModalContent.js
Normal file
237
frontend/src/MovieFile/Edit/FileEditModalContent.js
Normal file
@@ -0,0 +1,237 @@
|
||||
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 LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class FileEditModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
qualityId,
|
||||
languageIds,
|
||||
indexerFlags,
|
||||
proper,
|
||||
real,
|
||||
edition,
|
||||
releaseGroup
|
||||
} = props;
|
||||
|
||||
this.state = {
|
||||
qualityId,
|
||||
languageIds,
|
||||
indexerFlags,
|
||||
proper,
|
||||
real,
|
||||
edition,
|
||||
releaseGroup
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualityChange = ({ value }) => {
|
||||
this.setState({ qualityId: parseInt(value) });
|
||||
}
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.setState({ [name]: value });
|
||||
}
|
||||
|
||||
onSaveInputs = () => {
|
||||
this.props.onSaveInputs(this.state);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
qualities,
|
||||
languages,
|
||||
relativePath,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
qualityId,
|
||||
languageIds,
|
||||
indexerFlags,
|
||||
proper,
|
||||
real,
|
||||
edition,
|
||||
releaseGroup
|
||||
} = this.state;
|
||||
|
||||
const qualityOptions = qualities.map(({ id, name }) => {
|
||||
return {
|
||||
key: id,
|
||||
value: name
|
||||
};
|
||||
});
|
||||
|
||||
const languageOptions = languages.map(({ id, name }) => {
|
||||
return {
|
||||
key: id,
|
||||
value: name
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('EditMovieFile')} - {relativePath}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>
|
||||
{translate('UnableToLoadQualities')}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !error &&
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Quality')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="quality"
|
||||
value={qualityId}
|
||||
values={qualityOptions}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Proper')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="proper"
|
||||
value={proper}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Real')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="real"
|
||||
value={real}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Languages')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="languageIds"
|
||||
value={languageIds}
|
||||
values={languageOptions}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('IndexerFlags')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.INDEXER_FLAGS_SELECT}
|
||||
name="indexerFlags"
|
||||
indexerFlags={indexerFlags}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Edition')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="edition"
|
||||
value={edition}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ReleaseGroup')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="releaseGroup"
|
||||
value={releaseGroup}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
{translate('Cancel')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={this.onSaveInputs}
|
||||
>
|
||||
{translate('Save')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileEditModalContent.propTypes = {
|
||||
qualityId: PropTypes.number.isRequired,
|
||||
proper: PropTypes.bool.isRequired,
|
||||
real: PropTypes.bool.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
edition: PropTypes.string.isRequired,
|
||||
releaseGroup: PropTypes.string.isRequired,
|
||||
languageIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onSaveInputs: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default FileEditModalContent;
|
||||
139
frontend/src/MovieFile/Edit/FileEditModalContentConnector.js
Normal file
139
frontend/src/MovieFile/Edit/FileEditModalContentConnector.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { updateMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import createMovieFileSelector from 'Store/Selectors/createMovieFileSelector';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import FileEditModalContent from './FileEditModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createMovieFileSelector(),
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(state) => state.settings.languages,
|
||||
(movieFile, qualityProfiles, languages) => {
|
||||
|
||||
const filterItems = ['Any', 'Original'];
|
||||
const filteredLanguages = languages.items.filter((lang) => !filterItems.includes(lang.name));
|
||||
|
||||
const quality = movieFile.quality;
|
||||
|
||||
return {
|
||||
isFetching: qualityProfiles.isSchemaFetching || languages.isFetching,
|
||||
isPopulated: qualityProfiles.isSchemaPopulated && languages.isPopulated,
|
||||
error: qualityProfiles.error || languages.error,
|
||||
qualityId: quality ? quality.quality.id : 0,
|
||||
real: quality ? quality.revision.real > 0 : false,
|
||||
proper: quality ? quality.revision.version > 1 : false,
|
||||
qualities: getQualities(qualityProfiles.schema.items),
|
||||
languageIds: movieFile.languages ? movieFile.languages.map((l) => l.id) : [],
|
||||
languages: filteredLanguages,
|
||||
indexerFlags: movieFile.indexerFlags,
|
||||
edition: movieFile.edition,
|
||||
releaseGroup: movieFile.releaseGroup,
|
||||
relativePath: movieFile.relativePath
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchQualityProfileSchema: fetchQualityProfileSchema,
|
||||
dispatchUpdateMovieFiles: updateMovieFiles
|
||||
};
|
||||
|
||||
class FileEditModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount = () => {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchQualityProfileSchema();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSaveInputs = ( payload ) => {
|
||||
const {
|
||||
qualityId,
|
||||
real,
|
||||
proper,
|
||||
languageIds,
|
||||
edition,
|
||||
releaseGroup,
|
||||
indexerFlags
|
||||
} = payload;
|
||||
|
||||
const quality = this.props.qualities.find((item) => item.id === qualityId);
|
||||
|
||||
const languages = [];
|
||||
|
||||
languageIds.forEach((languageId) => {
|
||||
const language = this.props.languages.find((item) => item.id === parseInt(languageId));
|
||||
|
||||
if (language !== undefined) {
|
||||
languages.push(language);
|
||||
}
|
||||
});
|
||||
|
||||
const revision = {
|
||||
version: proper ? 2 : 1,
|
||||
real: real ? 1 : 0
|
||||
};
|
||||
|
||||
const movieFileIds = [this.props.movieFileId];
|
||||
|
||||
this.props.dispatchUpdateMovieFiles({
|
||||
movieFileIds,
|
||||
languages,
|
||||
indexerFlags,
|
||||
edition,
|
||||
releaseGroup,
|
||||
quality: {
|
||||
quality,
|
||||
revision
|
||||
}
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FileEditModalContent
|
||||
{...this.props}
|
||||
onSaveInputs={this.onSaveInputs}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileEditModalContentConnector.propTypes = {
|
||||
movieFileId: PropTypes.number.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
languageIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
indexerFlags: PropTypes.number.isRequired,
|
||||
qualityId: PropTypes.number.isRequired,
|
||||
real: PropTypes.bool.isRequired,
|
||||
edition: PropTypes.string.isRequired,
|
||||
releaseGroup: PropTypes.string.isRequired,
|
||||
relativePath: PropTypes.string.isRequired,
|
||||
proper: PropTypes.bool.isRequired,
|
||||
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchUpdateMovieFiles: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FileEditModalContentConnector);
|
||||
@@ -3,16 +3,14 @@ import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowCellButton from 'Components/Table/Cells/TableRowCellButton';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import SelectLanguageModal from 'MovieFile/Language/SelectLanguageModal';
|
||||
import FileEditModal from 'MovieFile/Edit/FileEditModal';
|
||||
import MediaInfoConnector from 'MovieFile/MediaInfoConnector';
|
||||
import * as mediaInfoTypes from 'MovieFile/mediaInfoTypes';
|
||||
import SelectQualityModal from 'MovieFile/Quality/SelectQualityModal';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FileDetailsModal from '../FileDetailsModal';
|
||||
@@ -28,32 +26,15 @@ class MovieFileEditorRow extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isSelectQualityModalOpen: false,
|
||||
isSelectLanguageModalOpen: false,
|
||||
isConfirmDeleteModalOpen: false,
|
||||
isFileDetailsModalOpen: false
|
||||
isFileDetailsModalOpen: false,
|
||||
isFileEditModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectQualityPress = () => {
|
||||
this.setState({ isSelectQualityModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectLanguagePress = () => {
|
||||
this.setState({ isSelectLanguageModalOpen: true });
|
||||
}
|
||||
|
||||
onSelectQualityModalClose = () => {
|
||||
this.setState({ isSelectQualityModalOpen: false });
|
||||
}
|
||||
|
||||
onSelectLanguageModalClose = () => {
|
||||
this.setState({ isSelectLanguageModalOpen: false });
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
}
|
||||
@@ -76,6 +57,14 @@ class MovieFileEditorRow extends Component {
|
||||
this.setState({ isFileDetailsModalOpen: false });
|
||||
}
|
||||
|
||||
onFileEditPress = () => {
|
||||
this.setState({ isFileEditModalOpen: true });
|
||||
}
|
||||
|
||||
onFileEditModalClose = () => {
|
||||
this.setState({ isFileEditModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
@@ -92,9 +81,8 @@ class MovieFileEditorRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isSelectQualityModalOpen,
|
||||
isSelectLanguageModalOpen,
|
||||
isFileDetailsModalOpen,
|
||||
isFileEditModalOpen,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
@@ -132,10 +120,8 @@ class MovieFileEditorRow extends Component {
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCellButton
|
||||
<TableRowCell
|
||||
className={styles.language}
|
||||
title={translate('ClickToChangeLanguage')}
|
||||
onPress={this.onSelectLanguagePress}
|
||||
>
|
||||
{
|
||||
showLanguagePlaceholder &&
|
||||
@@ -149,12 +135,10 @@ class MovieFileEditorRow extends Component {
|
||||
languages={languages}
|
||||
/>
|
||||
}
|
||||
</TableRowCellButton>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCellButton
|
||||
<TableRowCell
|
||||
className={styles.quality}
|
||||
title={translate('ClickToChangeQuality')}
|
||||
onPress={this.onSelectQualityPress}
|
||||
>
|
||||
{
|
||||
showQualityPlaceholder &&
|
||||
@@ -169,7 +153,7 @@ class MovieFileEditorRow extends Component {
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
}
|
||||
</TableRowCellButton>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.formats}
|
||||
@@ -180,6 +164,11 @@ class MovieFileEditorRow extends Component {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
name={icons.EDIT}
|
||||
onPress={this.onFileEditPress}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
name={icons.MEDIA_INFO}
|
||||
onPress={this.onFileDetailsPress}
|
||||
@@ -198,6 +187,12 @@ class MovieFileEditorRow extends Component {
|
||||
mediaInfo={mediaInfo}
|
||||
/>
|
||||
|
||||
<FileEditModal
|
||||
movieFileId={id}
|
||||
isOpen={isFileEditModalOpen}
|
||||
onModalClose={this.onFileEditModalClose}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
ids={[id]}
|
||||
@@ -208,22 +203,6 @@ class MovieFileEditorRow extends Component {
|
||||
onConfirm={this.onConfirmDelete}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={isSelectQualityModalOpen}
|
||||
ids={[id]}
|
||||
qualityId={quality ? quality.quality.id : 0}
|
||||
proper={quality ? quality.revision.version > 1 : false}
|
||||
real={quality ? quality.revision.real > 0 : false}
|
||||
onModalClose={this.onSelectQualityModalClose}
|
||||
/>
|
||||
|
||||
<SelectLanguageModal
|
||||
isOpen={isSelectLanguageModalOpen}
|
||||
ids={[id]}
|
||||
languageIds={languages ? languages.map((l) => l.id) : []}
|
||||
onModalClose={this.onSelectLanguageModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function createMapStateToProps() {
|
||||
items
|
||||
} = languages;
|
||||
|
||||
const filterItems = ['Any'];
|
||||
const filterItems = ['Any', 'Original'];
|
||||
const filteredLanguages = items.filter((lang) => !filterItems.includes(lang.name));
|
||||
|
||||
return {
|
||||
@@ -57,7 +57,9 @@ class SelectLanguageModalContentConnector extends Component {
|
||||
const language = _.find(this.props.items,
|
||||
(item) => item.id === parseInt(languageId));
|
||||
|
||||
languages.push(language);
|
||||
if (language !== undefined) {
|
||||
languages.push(language);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.dispatchupdateMovieFiles({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
|
||||
|
||||
@@ -143,7 +143,7 @@ EditRemotePathMappingModalContent.propTypes = {
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
item: PropTypes.shape(remotePathMappingShape).isRequired,
|
||||
downloadClientHosts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
downloadClientHosts: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
|
||||
@@ -49,6 +49,12 @@ function EditImportListModalContent(props) {
|
||||
fields
|
||||
} = item;
|
||||
|
||||
const importListTypeOptions = [
|
||||
{ key: 'manual', value: 'Manual' },
|
||||
{ key: 'automatic', value: 'Automatic Add' },
|
||||
{ key: 'exclusion', value: 'Exclusion List' }
|
||||
];
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
@@ -100,8 +106,9 @@ function EditImportListModalContent(props) {
|
||||
<FormLabel>{translate('EnableAutomaticAdd')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
type={inputTypes.SELECT}
|
||||
name="enableAuto"
|
||||
values={importListTypeOptions}
|
||||
helpText={translate('EnableAutoHelpText')}
|
||||
{...enableAuto}
|
||||
onChange={onInputChange}
|
||||
@@ -173,7 +180,7 @@ function EditImportListModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ListTagsHelpText')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -36,7 +36,7 @@ function EditRestrictionModalContent(props) {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{id ? 'Edit Restriction' : 'Add Restriction'}
|
||||
{id ? translate('EditRestriction') : translate('AddRestriction')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -51,7 +51,7 @@ function EditRestrictionModalContent(props) {
|
||||
name="required"
|
||||
helpText={translate('RequiredHelpText')}
|
||||
kind={kinds.SUCCESS}
|
||||
placeholder={translate('RequiredPlaceHolder')}
|
||||
placeholder={translate('RequiredRestrictionPlaceHolder')}
|
||||
{...required}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -116,8 +116,10 @@ class NamingModal extends Component {
|
||||
|
||||
const movieTokens = [
|
||||
{ token: '{Movie Title}', example: 'Movie Title!' },
|
||||
{ token: '{Movie Title:DE}', example: 'Filetitle' },
|
||||
{ token: '{Movie CleanTitle}', example: 'Movie Title' },
|
||||
{ token: '{Movie TitleThe}', example: 'Movie Title, The' },
|
||||
{ token: '{Movie OriginalTitle}', example: 'Τίτλος ταινίας' },
|
||||
{ token: '{Movie TitleFirstCharacter}', example: 'M' },
|
||||
{ token: '{Movie Collection}', example: 'The Movie Collection' },
|
||||
{ token: '{Movie Certification}', example: 'R' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { Component } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
import Link from 'Components/Link/Link';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.formGroupWrapper {
|
||||
.formGroupWrapper,
|
||||
.formatItemLarge {
|
||||
flex: 0 0 calc($formGroupSmallWidth - 100px);
|
||||
}
|
||||
|
||||
@@ -11,8 +12,20 @@
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointLarge) {
|
||||
.formatItemSmall {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: calc($breakpointLarge + 100px)) {
|
||||
.formGroupsContainer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.formatItemSmall {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.formatItemLarge {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,17 @@ import styles from './EditQualityProfileModalContent.css';
|
||||
|
||||
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
||||
|
||||
function getCustomFormatRender(formatItems, otherProps) {
|
||||
return (
|
||||
<QualityProfileFormatItems
|
||||
profileFormatItems={formatItems.value}
|
||||
errors={formatItems.errors}
|
||||
warnings={formatItems.warnings}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
class EditQualityProfileModalContent extends Component {
|
||||
|
||||
//
|
||||
@@ -251,6 +262,10 @@ class EditQualityProfileModalContent extends Component {
|
||||
onChange={onLanguageChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<div className={styles.formatItemLarge}>
|
||||
{getCustomFormatRender(formatItems, ...otherProps)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroupWrapper}>
|
||||
@@ -263,13 +278,8 @@ class EditQualityProfileModalContent extends Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroupWrapper}>
|
||||
<QualityProfileFormatItems
|
||||
profileFormatItems={formatItems.value}
|
||||
errors={formatItems.errors}
|
||||
warnings={formatItems.warnings}
|
||||
{...otherProps}
|
||||
/>
|
||||
<div className={styles.formatItemSmall}>
|
||||
{getCustomFormatRender(formatItems, otherProps)}
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -65,6 +65,8 @@ class UISettings extends Component {
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const uiLanguages = languages.filter((item) => item.value !== 'Original');
|
||||
|
||||
return (
|
||||
<PageContent title={translate('UISettings')}>
|
||||
<SettingsToolbarConnector
|
||||
@@ -213,7 +215,7 @@ class UISettings extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="uiLanguage"
|
||||
values={languages}
|
||||
values={uiLanguages}
|
||||
helpText={translate('UILanguageHelpText')}
|
||||
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
import { set, updateItem } from '../baseActions';
|
||||
|
||||
const abortCurrentRequests = {};
|
||||
let lastSaveData = null;
|
||||
|
||||
export function createCancelSaveProviderHandler(section) {
|
||||
return function(getState, payload, dispatch) {
|
||||
@@ -26,25 +28,33 @@ function createSaveProviderHandler(section, url, options = {}) {
|
||||
} = payload;
|
||||
|
||||
const saveData = getProviderState({ id, ...otherPayload }, getState, section);
|
||||
const requestUrl = id ? `${url}/${id}` : url;
|
||||
const params = { ...queryParams };
|
||||
|
||||
// If the user is re-saving the same provider without changes
|
||||
// force it to be saved. Only applies to editing existing providers.
|
||||
|
||||
if (id && _.isEqual(saveData, lastSaveData)) {
|
||||
params.forceSave = true;
|
||||
}
|
||||
|
||||
lastSaveData = saveData;
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}?${$.param(queryParams, true)}`,
|
||||
method: 'POST',
|
||||
url: `${requestUrl}?${$.param(params, true)}`,
|
||||
method: id ? 'PUT' : 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify(saveData)
|
||||
};
|
||||
|
||||
if (id) {
|
||||
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
|
||||
ajaxOptions.method = 'PUT';
|
||||
}
|
||||
|
||||
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
|
||||
|
||||
abortCurrentRequests[section] = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
lastSaveData = null;
|
||||
|
||||
dispatch(batchActions([
|
||||
updateItem({ section, ...data }),
|
||||
|
||||
|
||||
48
frontend/src/Store/Actions/Settings/indexerFlags.js
Normal file
48
frontend/src/Store/Actions/Settings/indexerFlags.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
const section = 'settings.indexerFlags';
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_INDEXER_FLAGS = 'settings/indexerFlags/fetchIndexerFlags';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchIndexerFlags = createThunk(FETCH_INDEXER_FLAGS);
|
||||
|
||||
//
|
||||
// Details
|
||||
|
||||
export default {
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
defaultState: {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
},
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
actionHandlers: {
|
||||
[FETCH_INDEXER_FLAGS]: createFetchHandler(section, '/indexerFlag')
|
||||
},
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
reducers: {
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
@@ -1,8 +1,11 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { set, updateItem } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createServerSideCollectionHandlers from './Creators/createServerSideCollectionHandlers';
|
||||
@@ -25,6 +28,7 @@ export const defaultState = {
|
||||
sortDirection: sortDirections.DESCENDING,
|
||||
error: null,
|
||||
items: [],
|
||||
isRemoving: false,
|
||||
|
||||
columns: [
|
||||
{
|
||||
@@ -96,7 +100,8 @@ export const GOTO_LAST_BLACKLIST_PAGE = 'blacklist/gotoBlacklistLastPage';
|
||||
export const GOTO_BLACKLIST_PAGE = 'blacklist/gotoBlacklistPage';
|
||||
export const SET_BLACKLIST_SORT = 'blacklist/setBlacklistSort';
|
||||
export const SET_BLACKLIST_TABLE_OPTION = 'blacklist/setBlacklistTableOption';
|
||||
export const REMOVE_FROM_BLACKLIST = 'blacklist/removeFromBlacklist';
|
||||
export const REMOVE_BLACKLIST_ITEM = 'blacklist/removeBlacklistItem';
|
||||
export const REMOVE_BLACKLIST_ITEMS = 'blacklist/removeBlacklistItems';
|
||||
export const CLEAR_BLACKLIST = 'blacklist/clearBlacklist';
|
||||
|
||||
//
|
||||
@@ -110,7 +115,8 @@ export const gotoBlacklistLastPage = createThunk(GOTO_LAST_BLACKLIST_PAGE);
|
||||
export const gotoBlacklistPage = createThunk(GOTO_BLACKLIST_PAGE);
|
||||
export const setBlacklistSort = createThunk(SET_BLACKLIST_SORT);
|
||||
export const setBlacklistTableOption = createAction(SET_BLACKLIST_TABLE_OPTION);
|
||||
export const removeFromBlacklist = createThunk(REMOVE_FROM_BLACKLIST);
|
||||
export const removeBlacklistItem = createThunk(REMOVE_BLACKLIST_ITEM);
|
||||
export const removeBlacklistItems = createThunk(REMOVE_BLACKLIST_ITEMS);
|
||||
export const clearBlacklist = createAction(CLEAR_BLACKLIST);
|
||||
|
||||
//
|
||||
@@ -131,7 +137,53 @@ export const actionHandlers = handleThunks({
|
||||
[serverSideCollectionHandlers.SORT]: SET_BLACKLIST_SORT
|
||||
}),
|
||||
|
||||
[REMOVE_FROM_BLACKLIST]: createRemoveItemHandler(section, '/blacklist')
|
||||
[REMOVE_BLACKLIST_ITEM]: createRemoveItemHandler(section, '/blacklist'),
|
||||
|
||||
[REMOVE_BLACKLIST_ITEMS]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
ids
|
||||
} = payload;
|
||||
|
||||
dispatch(batchActions([
|
||||
...ids.map((id) => {
|
||||
return updateItem({
|
||||
section,
|
||||
id,
|
||||
isRemoving: true
|
||||
});
|
||||
}),
|
||||
|
||||
set({ section, isRemoving: true })
|
||||
]));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/blacklist/bulk',
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({ ids })
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
// Don't use batchActions with thunks
|
||||
dispatch(fetchBlacklist());
|
||||
|
||||
dispatch(set({ section, isRemoving: false }));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(batchActions([
|
||||
...ids.map((id) => {
|
||||
return updateItem({
|
||||
section,
|
||||
id,
|
||||
isRemoving: false
|
||||
});
|
||||
}),
|
||||
|
||||
set({ section, isRemoving: false })
|
||||
]));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
|
||||
@@ -11,6 +11,7 @@ import * as history from './historyActions';
|
||||
import * as importMovie from './importMovieActions';
|
||||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
import * as movies from './movieActions';
|
||||
import * as movieBlacklist from './movieBlacklistActions';
|
||||
import * as movieCredits from './movieCreditsActions';
|
||||
import * as movieFiles from './movieFileActions';
|
||||
import * as movieHistory from './movieHistoryActions';
|
||||
@@ -48,6 +49,7 @@ export default [
|
||||
releases,
|
||||
rootFolders,
|
||||
movies,
|
||||
movieBlacklist,
|
||||
movieHistory,
|
||||
movieIndex,
|
||||
movieCredits,
|
||||
|
||||
82
frontend/src/Store/Actions/movieBlacklistActions.js
Normal file
82
frontend/src/Store/Actions/movieBlacklistActions.js
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { set, update } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'movieBlacklist';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_MOVIE_BLACKLIST = 'movieBlacklist/fetchMovieBlacklist';
|
||||
export const CLEAR_MOVIE_BLACKLIST = 'movieBlacklist/clearMovieBlacklist';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchMovieBlacklist = createThunk(FETCH_MOVIE_BLACKLIST);
|
||||
export const clearMovieBlacklist = createAction(CLEAR_MOVIE_BLACKLIST);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_MOVIE_BLACKLIST]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isFetching: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/blacklist/movie',
|
||||
data: payload
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_MOVIE_BLACKLIST]: (state) => {
|
||||
return Object.assign({}, state, defaultState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
||||
@@ -141,7 +141,10 @@ export const actionHandlers = handleThunks({
|
||||
const {
|
||||
movieFileIds,
|
||||
languages,
|
||||
quality
|
||||
indexerFlags,
|
||||
quality,
|
||||
edition,
|
||||
releaseGroup
|
||||
} = payload;
|
||||
|
||||
dispatch(set({ section, isSaving: true }));
|
||||
@@ -154,10 +157,22 @@ export const actionHandlers = handleThunks({
|
||||
data.languages = languages;
|
||||
}
|
||||
|
||||
if (indexerFlags !== undefined) {
|
||||
data.indexerFlags = indexerFlags;
|
||||
}
|
||||
|
||||
if (quality) {
|
||||
data.quality = quality;
|
||||
}
|
||||
|
||||
if (releaseGroup) {
|
||||
data.releaseGroup = releaseGroup;
|
||||
}
|
||||
|
||||
if (edition) {
|
||||
data.edition = edition;
|
||||
}
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/movieFile/editor',
|
||||
method: 'PUT',
|
||||
@@ -174,10 +189,22 @@ export const actionHandlers = handleThunks({
|
||||
props.languages = languages;
|
||||
}
|
||||
|
||||
if (indexerFlags) {
|
||||
props.indexerFlags = indexerFlags;
|
||||
}
|
||||
|
||||
if (quality) {
|
||||
props.quality = quality;
|
||||
}
|
||||
|
||||
if (edition) {
|
||||
props.edition = edition;
|
||||
}
|
||||
|
||||
if (releaseGroup) {
|
||||
props.releaseGroup = releaseGroup;
|
||||
}
|
||||
|
||||
return updateItem({ section, id, ...props });
|
||||
}),
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
@@ -142,6 +143,11 @@ export const defaultState = {
|
||||
isModifiable: false
|
||||
}
|
||||
]
|
||||
},
|
||||
sortPredicates: {
|
||||
estimatedCompletionTime: function(item, direction) {
|
||||
return moment.duration(item.timeleft).asMilliseconds();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -308,9 +314,9 @@ export const actionHandlers = handleThunks({
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
fetchQueue(),
|
||||
dispatch(fetchQueue());
|
||||
|
||||
dispatch(batchActions([
|
||||
...ids.map((id) => {
|
||||
return updateItem({
|
||||
section: paged,
|
||||
@@ -394,10 +400,10 @@ export const actionHandlers = handleThunks({
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
set({ section: paged, isRemoving: false }),
|
||||
fetchQueue()
|
||||
]));
|
||||
// Don't use batchActions with thunks
|
||||
dispatch(fetchQueue());
|
||||
|
||||
dispatch(set({ section: paged, isRemoving: false }));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
|
||||
@@ -10,6 +10,7 @@ import general from './Settings/general';
|
||||
import importExclusions from './Settings/importExclusions';
|
||||
import importListOptions from './Settings/importListOptions';
|
||||
import importLists from './Settings/importLists';
|
||||
import indexerFlags from './Settings/indexerFlags';
|
||||
import indexerOptions from './Settings/indexerOptions';
|
||||
import indexers from './Settings/indexers';
|
||||
import languages from './Settings/languages';
|
||||
@@ -31,6 +32,7 @@ export * from './Settings/delayProfiles';
|
||||
export * from './Settings/downloadClients';
|
||||
export * from './Settings/downloadClientOptions';
|
||||
export * from './Settings/general';
|
||||
export * from './Settings/indexerFlags';
|
||||
export * from './Settings/indexerOptions';
|
||||
export * from './Settings/indexers';
|
||||
export * from './Settings/languages';
|
||||
@@ -66,6 +68,7 @@ export const defaultState = {
|
||||
downloadClients: downloadClients.defaultState,
|
||||
downloadClientOptions: downloadClientOptions.defaultState,
|
||||
general: general.defaultState,
|
||||
indexerFlags: indexerFlags.defaultState,
|
||||
indexerOptions: indexerOptions.defaultState,
|
||||
indexers: indexers.defaultState,
|
||||
languages: languages.defaultState,
|
||||
@@ -109,6 +112,7 @@ export const actionHandlers = handleThunks({
|
||||
...downloadClients.actionHandlers,
|
||||
...downloadClientOptions.actionHandlers,
|
||||
...general.actionHandlers,
|
||||
...indexerFlags.actionHandlers,
|
||||
...indexerOptions.actionHandlers,
|
||||
...indexers.actionHandlers,
|
||||
...languages.actionHandlers,
|
||||
@@ -143,6 +147,7 @@ export const reducers = createHandleActions({
|
||||
...downloadClients.reducers,
|
||||
...downloadClientOptions.reducers,
|
||||
...general.reducers,
|
||||
...indexerFlags.reducers,
|
||||
...indexerOptions.reducers,
|
||||
...indexers.reducers,
|
||||
...languages.reducers,
|
||||
|
||||
@@ -19,6 +19,7 @@ function getInternalLink(source) {
|
||||
case 'IndexerRssCheck':
|
||||
case 'IndexerSearchCheck':
|
||||
case 'IndexerStatusCheck':
|
||||
case 'IndexerLongTermStatusCheck':
|
||||
return (
|
||||
<IconButton
|
||||
name={icons.SETTINGS}
|
||||
|
||||
@@ -22,7 +22,7 @@ class MoreInfo extends Component {
|
||||
|
||||
<DescriptionListItemTitle>Discord</DescriptionListItemTitle>
|
||||
<DescriptionListItemDescription>
|
||||
<Link to="https://discord.gg/AD3UP37">discord.gg/AD3UP37</Link>
|
||||
<Link to="https://discord.gg/r5wJPt9">discord.gg/r5wJPt9</Link>
|
||||
</DescriptionListItemDescription>
|
||||
|
||||
<DescriptionListItemTitle>Wiki</DescriptionListItemTitle>
|
||||
|
||||
@@ -24,9 +24,13 @@ class UpdateChanges extends Component {
|
||||
<ul>
|
||||
{
|
||||
changes.map((change, index) => {
|
||||
const checkChange = change.replace(/#\d{4,5}\b/g, (match, contents) => {
|
||||
return `[${match}](https://github.com/Radarr/Radarr/issues/${match.substring(1)})`;
|
||||
});
|
||||
|
||||
return (
|
||||
<li key={index}>
|
||||
<InlineMarkdown data={change} />
|
||||
<InlineMarkdown data={checkChange} />
|
||||
</li>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -7,7 +7,8 @@ function formatTimeSpan(timeSpan) {
|
||||
}
|
||||
|
||||
const duration = moment.duration(timeSpan);
|
||||
const days = duration.get('days');
|
||||
|
||||
const days = Math.floor(duration.asDays());
|
||||
const hours = padNumber(duration.get('hours'), 2);
|
||||
const minutes = padNumber(duration.get('minutes'), 2);
|
||||
const seconds = padNumber(duration.get('seconds'), 2);
|
||||
|
||||
@@ -4,6 +4,7 @@ import isInNextWeek from 'Utilities/Date/isInNextWeek';
|
||||
import isToday from 'Utilities/Date/isToday';
|
||||
import isTomorrow from 'Utilities/Date/isTomorrow';
|
||||
import isYesterday from 'Utilities/Date/isYesterday';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds = false, timeForToday = false } = {}) {
|
||||
if (!date) {
|
||||
@@ -21,15 +22,15 @@ function getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat,
|
||||
}
|
||||
|
||||
if (isYesterday(date)) {
|
||||
return 'Yesterday';
|
||||
return translate('Yesterday');
|
||||
}
|
||||
|
||||
if (isTodayDate) {
|
||||
return 'Today';
|
||||
return translate('Today');
|
||||
}
|
||||
|
||||
if (isTomorrow(date)) {
|
||||
return 'Tomorrow';
|
||||
return translate('Tomorrow');
|
||||
}
|
||||
|
||||
if (isInNextWeek(date)) {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isToday(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment(date).isSame(moment(), 'day');
|
||||
const dateObj = (typeof date === 'object') ? date : new Date(date);
|
||||
const today = new Date();
|
||||
|
||||
return dateObj.getDate() === today.getDate() && dateObj.getMonth() === today.getMonth() && dateObj.getFullYear() === today.getFullYear();
|
||||
}
|
||||
|
||||
export default isToday;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isTomorrow(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment(date).isSame(moment().add(1, 'day'), 'day');
|
||||
const dateObj = (typeof date === 'object') ? date : new Date(date);
|
||||
const today = new Date();
|
||||
const tomorrow = new Date((today.setDate(today.getDate() + 1)));
|
||||
|
||||
return dateObj.getDate() === tomorrow.getDate() && dateObj.getMonth() === tomorrow.getMonth() && dateObj.getFullYear() === tomorrow.getFullYear();
|
||||
}
|
||||
|
||||
export default isTomorrow;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import moment from 'moment';
|
||||
|
||||
function isYesterday(date) {
|
||||
if (!date) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return moment(date).isSame(moment().subtract(1, 'day'), 'day');
|
||||
const dateObj = (typeof date === 'object') ? date : new Date(date);
|
||||
const today = new Date();
|
||||
const yesterday = new Date((today.setDate(today.getDate() - 1)));
|
||||
|
||||
return dateObj.getDate() === yesterday.getDate() && dateObj.getMonth() === yesterday.getMonth() && dateObj.getFullYear() === yesterday.getFullYear();
|
||||
}
|
||||
|
||||
export default isYesterday;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import _ from 'lodash';
|
||||
import qs from 'qs';
|
||||
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils
|
||||
@@ -10,18 +9,17 @@ export default function parseUrl(url) {
|
||||
// The `origin`, `password`, and `username` properties are unavailable in
|
||||
// Opera Presto. We synthesize `origin` if it's not present. While `password`
|
||||
// and `username` are ignored intentionally.
|
||||
const properties = _.pick(
|
||||
anchor,
|
||||
'hash',
|
||||
'host',
|
||||
'hostname',
|
||||
'href',
|
||||
'origin',
|
||||
'pathname',
|
||||
'port',
|
||||
'protocol',
|
||||
'search'
|
||||
);
|
||||
const properties = {
|
||||
hash: anchor.hash,
|
||||
host: anchor.host,
|
||||
hostname: anchor.hostname,
|
||||
href: anchor.href,
|
||||
origin: anchor.origin,
|
||||
pathname: anchor.pathname,
|
||||
port: anchor.port,
|
||||
protocol: anchor.protocol,
|
||||
search: anchor.search
|
||||
};
|
||||
|
||||
properties.isAbsolute = (/^[\w:]*\/\//).test(url);
|
||||
|
||||
|
||||
62
macOS/Radarr
62
macOS/Radarr
@@ -1,62 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
#get the bundle's MacOS directory full path
|
||||
DIR=$(cd "$(dirname "$0")"; pwd)
|
||||
|
||||
#change these values to match your app
|
||||
EXE_PATH="$DIR/Radarr.exe"
|
||||
APPNAME="Radarr"
|
||||
|
||||
#set up environment
|
||||
if [[ -x '/opt/local/bin/mono' ]]; then
|
||||
# Macports and mono-supplied installer path
|
||||
export PATH="/opt/local/bin:$PATH"
|
||||
elif [[ -x '/usr/local/bin/mono' ]]; then
|
||||
# Homebrew-supplied path to mono
|
||||
export PATH="/usr/local/bin:$PATH"
|
||||
fi
|
||||
|
||||
export DYLD_FALLBACK_LIBRARY_PATH="$DIR"
|
||||
|
||||
if [ -e /Library/Frameworks/Mono.framework ]; then
|
||||
MONO_FRAMEWORK_PATH=/Library/Frameworks/Mono.framework/Versions/Current
|
||||
export PATH="$MONO_FRAMEWORK_PATH/bin:$PATH"
|
||||
export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:$MONO_FRAMEWORK_PATH/lib"
|
||||
fi
|
||||
|
||||
if [[ -f '/opt/local/lib/libsqlite3.0.dylib' ]]; then
|
||||
export DYLD_FALLBACK_LIBRARY_PATH="/opt/local/lib:$DYLD_FALLBACK_LIBRARY_PATH"
|
||||
fi
|
||||
|
||||
export DYLD_FALLBACK_LIBRARY_PATH="$DYLD_FALLBACK_LIBRARY_PATH:$HOME/lib:/usr/local/lib:/lib:/usr/lib"
|
||||
|
||||
#mono version check
|
||||
REQUIRED_MAJOR=4
|
||||
REQUIRED_MINOR=6
|
||||
|
||||
VERSION_TITLE="Cannot launch $APPNAME"
|
||||
VERSION_MSG="$APPNAME requires Mono Runtime Environment(MRE) $REQUIRED_MAJOR.$REQUIRED_MINOR or later."
|
||||
DOWNLOAD_URL="http://www.mono-project.com/download/#download-mac"
|
||||
|
||||
MONO_VERSION="$(mono --version | grep 'Mono JIT compiler version ' | cut -f5 -d\ )"
|
||||
# if [[ -o DEBUG ]]; then osascript -e "display dialog \"MONO_VERSION: $MONO_VERSION\""; fi
|
||||
|
||||
|
||||
MONO_VERSION_MAJOR="$(echo $MONO_VERSION | cut -f1 -d.)"
|
||||
MONO_VERSION_MINOR="$(echo $MONO_VERSION | cut -f2 -d.)"
|
||||
if [ -z "$MONO_VERSION" ] \
|
||||
|| [ $MONO_VERSION_MAJOR -lt $REQUIRED_MAJOR ] \
|
||||
|| [ $MONO_VERSION_MAJOR -eq $REQUIRED_MAJOR -a $MONO_VERSION_MINOR -lt $REQUIRED_MINOR ]
|
||||
then
|
||||
osascript \
|
||||
-e "set question to display dialog \"$VERSION_MSG\" with title \"$VERSION_TITLE\" buttons {\"Cancel\", \"Download...\"} default button 2" \
|
||||
-e "if button returned of question is equal to \"Download...\" then open location \"$DOWNLOAD_URL\""
|
||||
echo "$VERSION_TITLE"
|
||||
echo "$VERSION_MSG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MONO_EXEC="exec mono --debug"
|
||||
|
||||
#run app using mono
|
||||
$MONO_EXEC "$EXE_PATH"
|
||||
83
package.json
83
package.json
@@ -17,28 +17,27 @@
|
||||
"license": "GPL-3.0",
|
||||
"readmeFilename": "readme.md",
|
||||
"dependencies": {
|
||||
"@babel/core": "7.9.6",
|
||||
"@babel/plugin-proposal-class-properties": "7.8.3",
|
||||
"@babel/plugin-proposal-decorators": "7.8.3",
|
||||
"@babel/plugin-proposal-export-default-from": "7.8.3",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.8.3",
|
||||
"@babel/plugin-proposal-function-sent": "7.8.3",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.8.3",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.8.3",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.9.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.8.3",
|
||||
"@babel/core": "7.11.6",
|
||||
"@babel/plugin-proposal-class-properties": "7.10.4",
|
||||
"@babel/plugin-proposal-decorators": "7.10.5",
|
||||
"@babel/plugin-proposal-export-default-from": "7.10.4",
|
||||
"@babel/plugin-proposal-export-namespace-from": "7.10.4",
|
||||
"@babel/plugin-proposal-function-sent": "7.10.4",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator": "7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "7.10.4",
|
||||
"@babel/plugin-proposal-optional-chaining": "7.11.0",
|
||||
"@babel/plugin-proposal-throw-expressions": "7.10.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "7.8.3",
|
||||
"@babel/preset-env": "7.9.6",
|
||||
"@babel/preset-react": "7.9.4",
|
||||
"@fortawesome/fontawesome-free": "5.13.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||
"@fortawesome/free-regular-svg-icons": "5.13.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.13.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.9",
|
||||
"@babel/preset-env": "7.11.5",
|
||||
"@babel/preset-react": "7.10.4",
|
||||
"@fortawesome/fontawesome-free": "5.15.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.31",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.0",
|
||||
"@fortawesome/react-fontawesome": "0.1.11",
|
||||
"@microsoft/signalr": "3.1.7",
|
||||
"@popperjs/core": "2.2.1",
|
||||
"@sentry/browser": "5.15.5",
|
||||
"@sentry/integrations": "5.15.5",
|
||||
"@sentry/browser": "5.24.2",
|
||||
"@sentry/integrations": "5.24.2",
|
||||
"ansi-colors": "4.1.1",
|
||||
"autoprefixer": "9.7.5",
|
||||
"babel-eslint": "10.1.0",
|
||||
@@ -48,19 +47,19 @@
|
||||
"classnames": "2.2.6",
|
||||
"clipboard": "2.0.6",
|
||||
"connected-react-router": "6.8.0",
|
||||
"core-js": "3",
|
||||
"core-js": "3.6.5",
|
||||
"css-loader": "3.4.2",
|
||||
"del": "5.1.0",
|
||||
"del": "6.0.0",
|
||||
"element-class": "0.2.2",
|
||||
"eslint": "7.5.0",
|
||||
"eslint": "7.10.0",
|
||||
"eslint-plugin-filenames": "1.3.2",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-react": "7.20.5",
|
||||
"eslint-plugin-react": "7.21.3",
|
||||
"eslint-plugin-simple-import-sort": "5.0.3",
|
||||
"esprint": "0.7.0",
|
||||
"file-loader": "6.0.0",
|
||||
"file-loader": "6.1.0",
|
||||
"filesize": "6.1.0",
|
||||
"fuse.js": "6.0.4",
|
||||
"fuse.js": "6.4.1",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-cached": "1.1.1",
|
||||
"gulp-concat": "2.6.1",
|
||||
@@ -71,14 +70,14 @@
|
||||
"gulp-watch": "5.0.1",
|
||||
"gulp-wrap": "0.15.0",
|
||||
"history": "4.10.1",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"html-webpack-plugin": "4.5.0",
|
||||
"jdu": "1.0.0",
|
||||
"jquery": "3.5.1",
|
||||
"loader-utils": "^2.0.0",
|
||||
"lodash": "4.17.20",
|
||||
"mini-css-extract-plugin": "0.9.0",
|
||||
"mobile-detect": "1.4.4",
|
||||
"moment": "2.24.0",
|
||||
"moment": "2.29.0",
|
||||
"mousetrap": "1.6.5",
|
||||
"normalize.css": "8.0.1",
|
||||
"optimize-css-assets-webpack-plugin": "5.0.3",
|
||||
@@ -92,22 +91,22 @@
|
||||
"qs": "6.9.4",
|
||||
"react": "16.13.1",
|
||||
"react-addons-shallow-compare": "15.6.2",
|
||||
"react-async-script": "1.1.1",
|
||||
"react-async-script": "1.2.0",
|
||||
"react-autosuggest": "10.0.2",
|
||||
"react-custom-scrollbars": "4.2.1",
|
||||
"react-dnd": "10.0.2",
|
||||
"react-dnd-html5-backend": "10.0.2",
|
||||
"react-dnd": "11.1.3",
|
||||
"react-dnd-html5-backend": "11.1.3",
|
||||
"react-document-title": "2.0.3",
|
||||
"react-dom": "16.13.1",
|
||||
"react-focus-lock": "2.3.1",
|
||||
"react-google-recaptcha": "2.0.1",
|
||||
"react-lazyload": "2.6.7",
|
||||
"react-focus-lock": "2.4.1",
|
||||
"react-google-recaptcha": "2.1.0",
|
||||
"react-lazyload": "3.0.0",
|
||||
"react-measure": "1.4.7",
|
||||
"react-popper": "1.3.7",
|
||||
"react-redux": "7.2.0",
|
||||
"react-router": "5.1.2",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-slider": "1.0.7",
|
||||
"react-redux": "7.2.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-slider": "1.0.11",
|
||||
"react-tabs": "3.1.1",
|
||||
"react-text-truncate": "0.16.0",
|
||||
"react-virtualized": "9.21.1",
|
||||
@@ -121,12 +120,12 @@
|
||||
"run-sequence": "2.2.1",
|
||||
"streamqueue": "1.1.2",
|
||||
"style-loader": "1.2.1",
|
||||
"stylelint": "13.6.1",
|
||||
"stylelint": "13.7.2",
|
||||
"stylelint-order": "4.1.0",
|
||||
"url-loader": "4.1.0",
|
||||
"webpack": "4.42.1",
|
||||
"webpack-stream": "5.2.1",
|
||||
"worker-loader": "2.0.0"
|
||||
"webpack": "4.44.2",
|
||||
"webpack-stream": "6.1.0",
|
||||
"worker-loader": "3.0.3"
|
||||
},
|
||||
"main": "index.js",
|
||||
"browserslist": [
|
||||
|
||||
@@ -37,13 +37,14 @@ Compression=lzma2/normal
|
||||
AppContact={#ForumsURL}
|
||||
VersionInfoVersion={#BaseVersion}.{#BuildNumber}
|
||||
SetupLogging=yes
|
||||
OutputDir=output
|
||||
|
||||
[Languages]
|
||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||
|
||||
[Tasks]
|
||||
Name: "desktopIcon"; Description: "{cm:CreateDesktopIcon}"
|
||||
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts)"; GroupDescription: "Start automatically"; Flags: exclusive
|
||||
Name: "windowsService"; Description: "Install Windows Service (Starts when the computer starts as the LocalService user, you will need to change the user to access network shares)"; GroupDescription: "Start automatically"; Flags: exclusive
|
||||
Name: "startupShortcut"; Description: "Create shortcut in Startup folder (Starts when you log into Windows)"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
Name: "none"; Description: "Do not start automatically"; GroupDescription: "Start automatically"; Flags: exclusive unchecked
|
||||
|
||||
@@ -57,6 +58,9 @@ Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"
|
||||
Name: "{commondesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "/icon"; Tasks: desktopIcon
|
||||
Name: "{userstartup}\{#AppName}"; Filename: "{app}\Radarr.exe"; WorkingDir: "{app}"; Tasks: startupShortcut
|
||||
|
||||
[InstallDelete]
|
||||
Name: "{app}"; Type: filesandordirs
|
||||
|
||||
[Run]
|
||||
Filename: "{app}\Radarr.Console.exe"; StatusMsg: "Removing previous Windows Service"; Parameters: "/u"; Flags: runhidden waituntilterminated;
|
||||
Filename: "{app}\Radarr.Console.exe"; Description: "Enable Access from Other Devices"; StatusMsg: "Enabling Remote access"; Parameters: "/registerurl"; Flags: postinstall runascurrentuser runhidden waituntilterminated; Tasks: startupShortcut none;
|
||||
@@ -72,5 +76,6 @@ function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||||
var
|
||||
ResultCode: Integer;
|
||||
begin
|
||||
Exec(ExpandConstant('{commonappdata}\Radarr\bin\Radarr.Console.exe'), '/u', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||
Exec('net', 'stop radarr', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||
Exec('sc', 'delete radarr', '', 0, ewWaitUntilTerminated, ResultCode)
|
||||
end;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Nancy;
|
||||
@@ -57,7 +58,7 @@ namespace NzbDrone.Api.FileSystem
|
||||
|
||||
if (!_diskProvider.FolderExists(path))
|
||||
{
|
||||
return new string[0];
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
return _diskScanService.GetVideoFiles(path).Select(f => new
|
||||
|
||||
@@ -56,9 +56,7 @@ namespace NzbDrone.Api.Indexers
|
||||
public int? Leechers { get; set; }
|
||||
public DownloadProtocol Protocol { get; set; }
|
||||
|
||||
// TODO: Remove in v3
|
||||
// Used to support the original Release Push implementation
|
||||
// JsonIgnore so we don't serialize it, but can still parse it
|
||||
// JsonIgnore so we don't serialize it, but can still parse it, removed in v3
|
||||
[JsonIgnore]
|
||||
public DownloadProtocol DownloadProtocol
|
||||
{
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Api.ImportList
|
||||
base.MapToResource(resource, definition);
|
||||
|
||||
resource.Enabled = definition.Enabled;
|
||||
resource.EnableAuto = definition.EnableAuto;
|
||||
resource.EnableAuto = (int)definition.EnableAuto;
|
||||
resource.ProfileId = definition.ProfileId;
|
||||
resource.RootFolderPath = definition.RootFolderPath;
|
||||
resource.ShouldMonitor = definition.ShouldMonitor;
|
||||
@@ -34,7 +34,7 @@ namespace NzbDrone.Api.ImportList
|
||||
base.MapToModel(definition, resource);
|
||||
|
||||
definition.Enabled = resource.Enabled;
|
||||
definition.EnableAuto = resource.EnableAuto;
|
||||
definition.EnableAuto = (ImportListType)resource.EnableAuto;
|
||||
definition.ProfileId = resource.ProfileId;
|
||||
definition.RootFolderPath = resource.RootFolderPath;
|
||||
definition.ShouldMonitor = resource.ShouldMonitor;
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace NzbDrone.Api.ImportList
|
||||
public class ImportListResource : ProviderResource
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
public bool EnableAuto { get; set; }
|
||||
public int EnableAuto { get; set; }
|
||||
public bool ShouldMonitor { get; set; }
|
||||
public string RootFolderPath { get; set; }
|
||||
public int ProfileId { get; set; }
|
||||
|
||||
@@ -147,7 +147,7 @@ namespace NzbDrone.Api
|
||||
{
|
||||
var defaultDefinitions = _providerFactory.GetDefaultDefinitions().OrderBy(p => p.ImplementationName).ToList();
|
||||
|
||||
var result = new List<TProviderResource>(defaultDefinitions.Count());
|
||||
var result = new List<TProviderResource>(defaultDefinitions.Count);
|
||||
|
||||
foreach (var providerDefinition in defaultDefinitions)
|
||||
{
|
||||
|
||||
@@ -42,6 +42,8 @@ namespace NzbDrone.Automation.Test
|
||||
// Timeout as windows automation tests seem to take alot longer to get going
|
||||
driver = new ChromeDriver(service, options, new TimeSpan(0, 3, 0));
|
||||
|
||||
driver.Manage().Window.Size = new System.Drawing.Size(1920, 1080);
|
||||
|
||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger());
|
||||
_runner.KillAll();
|
||||
_runner.Start();
|
||||
@@ -62,6 +64,19 @@ namespace NzbDrone.Automation.Test
|
||||
.Select(e => e.Text);
|
||||
}
|
||||
|
||||
protected void TakeScreenshot(string name)
|
||||
{
|
||||
try
|
||||
{
|
||||
Screenshot image = ((ITakesScreenshot)driver).GetScreenshot();
|
||||
image.SaveAsFile($"./{name}_test_screenshot.png", ScreenshotImageFormat.Png);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to save screenshot {name}, {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void SmokeTestTearDown()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using FluentAssertions;
|
||||
using System.Reflection;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Automation.Test.PageModel;
|
||||
using OpenQA.Selenium;
|
||||
@@ -21,6 +22,10 @@ namespace NzbDrone.Automation.Test
|
||||
{
|
||||
_page.MovieNavIcon.Click();
|
||||
_page.WaitForNoSpinner();
|
||||
|
||||
var imageName = MethodBase.GetCurrentMethod().Name;
|
||||
TakeScreenshot(imageName);
|
||||
|
||||
_page.Find(By.CssSelector("div[class*='MovieIndex']")).Should().NotBeNull();
|
||||
}
|
||||
|
||||
@@ -30,6 +35,9 @@ namespace NzbDrone.Automation.Test
|
||||
_page.CalendarNavIcon.Click();
|
||||
_page.WaitForNoSpinner();
|
||||
|
||||
var imageName = MethodBase.GetCurrentMethod().Name;
|
||||
TakeScreenshot(imageName);
|
||||
|
||||
_page.Find(By.CssSelector("div[class*='CalendarPage']")).Should().NotBeNull();
|
||||
}
|
||||
|
||||
@@ -39,6 +47,9 @@ namespace NzbDrone.Automation.Test
|
||||
_page.ActivityNavIcon.Click();
|
||||
_page.WaitForNoSpinner();
|
||||
|
||||
var imageName = MethodBase.GetCurrentMethod().Name;
|
||||
TakeScreenshot(imageName);
|
||||
|
||||
_page.Find(By.LinkText("Queue")).Should().NotBeNull();
|
||||
_page.Find(By.LinkText("History")).Should().NotBeNull();
|
||||
_page.Find(By.LinkText("Blacklist")).Should().NotBeNull();
|
||||
@@ -50,6 +61,9 @@ namespace NzbDrone.Automation.Test
|
||||
_page.SystemNavIcon.Click();
|
||||
_page.WaitForNoSpinner();
|
||||
|
||||
var imageName = MethodBase.GetCurrentMethod().Name;
|
||||
TakeScreenshot(imageName);
|
||||
|
||||
_page.Find(By.CssSelector("div[class*='Health']")).Should().NotBeNull();
|
||||
}
|
||||
|
||||
@@ -58,11 +72,12 @@ namespace NzbDrone.Automation.Test
|
||||
{
|
||||
_page.MovieNavIcon.Click();
|
||||
_page.WaitForNoSpinner();
|
||||
|
||||
_page.Find(By.LinkText("Add New")).Click();
|
||||
|
||||
_page.WaitForNoSpinner();
|
||||
|
||||
var imageName = MethodBase.GetCurrentMethod().Name;
|
||||
TakeScreenshot(imageName);
|
||||
|
||||
_page.Find(By.CssSelector("input[class*='AddNewMovie-searchInput']")).Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user