mirror of
https://github.com/Radarr/Radarr.git
synced 2026-04-18 21:35:51 -04:00
Compare commits
189 Commits
v5.0.1.799
...
v5.2.2.828
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c96b3c4b0b | ||
|
|
78bc9f9b4b | ||
|
|
b737f05a83 | ||
|
|
8d96fd2387 | ||
|
|
6c487ead00 | ||
|
|
22e3cf844c | ||
|
|
14b4b5e122 | ||
|
|
10f5f3c5c8 | ||
|
|
c687b552f0 | ||
|
|
92c8c8a7f5 | ||
|
|
86a16c3c0c | ||
|
|
e8c280db34 | ||
|
|
ea65e2174c | ||
|
|
4aa2466693 | ||
|
|
4df0f0f721 | ||
|
|
d7bee375e8 | ||
|
|
906295466d | ||
|
|
f86060eca2 | ||
|
|
bf9a0b62f2 | ||
|
|
ccc62f0450 | ||
|
|
524657ad78 | ||
|
|
7a394ff864 | ||
|
|
d8862eedd3 | ||
|
|
71f700e240 | ||
|
|
ae5dd84e0a | ||
|
|
17b398cf62 | ||
|
|
d00678c1ba | ||
|
|
03df9b7f07 | ||
|
|
3442a0ecca | ||
|
|
3376a467ca | ||
|
|
1650ce17fb | ||
|
|
2f2004faa2 | ||
|
|
437e2f4597 | ||
|
|
17b8605751 | ||
|
|
b2a52e52b6 | ||
|
|
0f5fabdfcd | ||
|
|
6362ee9b7d | ||
|
|
50465fd482 | ||
|
|
54d447d55f | ||
|
|
50f48277e5 | ||
|
|
c2e206b7ac | ||
|
|
7a46de602f | ||
|
|
89820c1ff7 | ||
|
|
67e6e129ff | ||
|
|
b6001238e5 | ||
|
|
7bbdcc81bb | ||
|
|
3e8cbc497e | ||
|
|
60d2df043b | ||
|
|
da41cb8840 | ||
|
|
a4b7c99d91 | ||
|
|
8fb21e073b | ||
|
|
dbf424d454 | ||
|
|
a6dda70c0a | ||
|
|
e6fa14b1e6 | ||
|
|
b5c0d515ee | ||
|
|
b7aee25d0d | ||
|
|
233b85aaf3 | ||
|
|
80db9a7dd4 | ||
|
|
660d3d7643 | ||
|
|
d999aea36f | ||
|
|
5d45f1de89 | ||
|
|
3e5089719c | ||
|
|
ec69dfaabb | ||
|
|
aa13a40bad | ||
|
|
9b458812f1 | ||
|
|
1bdc48a889 | ||
|
|
e5d479a162 | ||
|
|
9a50fcb82a | ||
|
|
f2357e0b60 | ||
|
|
0591d05c3b | ||
|
|
299d50d56c | ||
|
|
7d3c01114b | ||
|
|
70376af70b | ||
|
|
9ef031bd9e | ||
|
|
3a9b276c43 | ||
|
|
aabf209a07 | ||
|
|
79c03f2fe6 | ||
|
|
9b36404071 | ||
|
|
ecfaea3885 | ||
|
|
bfbeb4c62e | ||
|
|
4b98d27f31 | ||
|
|
604d74270d | ||
|
|
15bb9139d1 | ||
|
|
32722eb704 | ||
|
|
e0c8a8f0d6 | ||
|
|
a3bb0541f0 | ||
|
|
e78bc34514 | ||
|
|
35c4538288 | ||
|
|
3981e816cd | ||
|
|
9354031571 | ||
|
|
a01328dc8c | ||
|
|
8cb6295ddc | ||
|
|
99f7d8bcf5 | ||
|
|
f13d479b88 | ||
|
|
23eb637bc3 | ||
|
|
3a786d0b9d | ||
|
|
6fb127235c | ||
|
|
5517e578b6 | ||
|
|
bced2e7b2e | ||
|
|
f7313369b5 | ||
|
|
b14e93e11f | ||
|
|
f5692d6cf1 | ||
|
|
a2d505c795 | ||
|
|
3d46bd2d8f | ||
|
|
017f272201 | ||
|
|
c221e2097a | ||
|
|
a61804e949 | ||
|
|
cb2bed93cb | ||
|
|
2bea61bae5 | ||
|
|
7922109f01 | ||
|
|
46dd72e0cd | ||
|
|
4e3535f1fe | ||
|
|
3468f1144d | ||
|
|
572c410f54 | ||
|
|
1762a189d2 | ||
|
|
e2f5f2f73a | ||
|
|
ade387ba74 | ||
|
|
6b9a622328 | ||
|
|
ba5028bebb | ||
|
|
33d1d1f875 | ||
|
|
fb60dcb5bf | ||
|
|
ddf23530fc | ||
|
|
30b1edbff0 | ||
|
|
f20c260a4f | ||
|
|
2fcbac49c7 | ||
|
|
3248e7f476 | ||
|
|
ce145a3050 | ||
|
|
3bc4197b4a | ||
|
|
552b8f91d2 | ||
|
|
e9e36ae56a | ||
|
|
450d6c0c80 | ||
|
|
9eece2965a | ||
|
|
cd5d4f993a | ||
|
|
fe7203815d | ||
|
|
4e01fa57fd | ||
|
|
bbeb4d7b5f | ||
|
|
49dac0ebaa | ||
|
|
ea8f5c7b9f | ||
|
|
24a17a9240 | ||
|
|
97c2d4f9db | ||
|
|
b7cafb2917 | ||
|
|
2a2667a2ec | ||
|
|
27da524391 | ||
|
|
4bd1c14db9 | ||
|
|
608e2e7307 | ||
|
|
cff54d76b9 | ||
|
|
3244282a83 | ||
|
|
1d488df242 | ||
|
|
22927224c6 | ||
|
|
51149bccdd | ||
|
|
4bbc166040 | ||
|
|
11c7446cbe | ||
|
|
dce637905a | ||
|
|
7d85922f8d | ||
|
|
80f6033595 | ||
|
|
78b8747b50 | ||
|
|
c2df194d49 | ||
|
|
4a41c67dfe | ||
|
|
85d51e485a | ||
|
|
50e2e9edef | ||
|
|
703c251b5c | ||
|
|
a798556d32 | ||
|
|
69253a4ac4 | ||
|
|
4e827e726f | ||
|
|
e3abda9afc | ||
|
|
0386ea9b71 | ||
|
|
f0fcd23248 | ||
|
|
18f22d7ada | ||
|
|
1c4b5f2abf | ||
|
|
b48b970f25 | ||
|
|
e715557a0d | ||
|
|
248ac9619c | ||
|
|
feff609685 | ||
|
|
07cfbb59da | ||
|
|
9db0058114 | ||
|
|
8d7f6b9de8 | ||
|
|
28c566a071 | ||
|
|
e5963c9ee1 | ||
|
|
336cb4a2bc | ||
|
|
ff3d38a515 | ||
|
|
a2bde5e016 | ||
|
|
cb04ef960e | ||
|
|
ba732847ef | ||
|
|
1865257544 | ||
|
|
58e0b19d06 | ||
|
|
05c5bcbe15 | ||
|
|
d6749a0c8e | ||
|
|
72fe25d7b2 | ||
|
|
0598d46ee8 |
16
.github/label-actions.yml
vendored
Normal file
16
.github/label-actions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||||
|
|
||||||
|
'Type: Support':
|
||||||
|
comment: >
|
||||||
|
:wave: @{issue-author}, 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://radarr.video/discord).
|
||||||
|
close: true
|
||||||
|
close-reason: 'not planned'
|
||||||
|
|
||||||
|
'Status: Logs Needed':
|
||||||
|
comment: >
|
||||||
|
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||||
|
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||||
|
Guidance on how to enable trace logging can be found in
|
||||||
|
our [troubleshooting guide](https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files).
|
||||||
17
.github/workflows/label-actions.yml
vendored
Normal file
17
.github/workflows/label-actions.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: 'Label Actions'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [labeled, unlabeled]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
action:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: dessant/label-actions@v3
|
||||||
|
with:
|
||||||
|
process-only: 'issues'
|
||||||
36
.github/workflows/support.yml
vendored
36
.github/workflows/support.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
name: 'Support requests'
|
|
||||||
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [labeled, unlabeled, reopened]
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
jobs:
|
|
||||||
support:
|
|
||||||
permissions:
|
|
||||||
issues: write # to modify issues
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: dessant/support-requests@v3
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
support-label: 'Type: Support'
|
|
||||||
issue-comment: >
|
|
||||||
:wave: @{issue-author}, 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://radarr.video/discord).
|
|
||||||
close-issue: true
|
|
||||||
close-reason: 'not planned'
|
|
||||||
lock-issue: false
|
|
||||||
- uses: dessant/support-requests@v3
|
|
||||||
with:
|
|
||||||
github-token: ${{ github.token }}
|
|
||||||
support-label: 'Status: Logs Needed'
|
|
||||||
issue-comment: >
|
|
||||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
|
||||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
|
||||||
Guidance on how to enable trace logging can be found in
|
|
||||||
our [troubleshooting guide](https://wiki.servarr.com/radarr/troubleshooting#logging-and-log-files).
|
|
||||||
close-issue: false
|
|
||||||
lock-issue: false
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
[](https://dev.azure.com/Radarr/Radarr/_build/latest?definitionId=1&branchName=develop)
|
||||||
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
[](https://translate.servarr.com/engage/radarr/?utm_source=widget)
|
||||||
[](https://wiki.servarr.com/radarr/installation#docker)
|
[](https://wiki.servarr.com/radarr/installation/docker)
|
||||||

|

|
||||||
[](#backers)
|
[](#backers)
|
||||||
[](#sponsors)
|
[](#sponsors)
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ variables:
|
|||||||
testsFolder: './_tests'
|
testsFolder: './_tests'
|
||||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||||
majorVersion: '5.0.1'
|
majorVersion: '5.2.2'
|
||||||
minorVersion: $[counter('minorVersion', 2000)]
|
minorVersion: $[counter('minorVersion', 2000)]
|
||||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||||
sentryOrg: 'servarr'
|
sentryOrg: 'servarr'
|
||||||
sentryUrl: 'https://sentry.servarr.com'
|
sentryUrl: 'https://sentry.servarr.com'
|
||||||
dotnetVersion: '6.0.408'
|
dotnetVersion: '6.0.413'
|
||||||
nodeVersion: '16.X'
|
nodeVersion: '16.X'
|
||||||
innoVersion: '6.2.0'
|
innoVersion: '6.2.0'
|
||||||
windowsImage: 'windows-2022'
|
windowsImage: 'windows-2022'
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
// Stage 1
|
// Stage 1
|
||||||
'@babel/plugin-proposal-export-default-from',
|
'@babel/plugin-proposal-export-default-from',
|
||||||
['@babel/plugin-proposal-optional-chaining', { loose }],
|
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||||
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
|
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
|
||||||
|
|
||||||
// Stage 2
|
// Stage 2
|
||||||
'@babel/plugin-proposal-export-namespace-from',
|
'@babel/plugin-transform-export-namespace-from',
|
||||||
|
|
||||||
// Stage 3
|
// Stage 3
|
||||||
['@babel/plugin-proposal-class-properties', { loose }],
|
['@babel/plugin-transform-class-properties', { loose }],
|
||||||
'@babel/plugin-syntax-dynamic-import'
|
'@babel/plugin-syntax-dynamic-import'
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class Blocklist extends Component {
|
|||||||
lastToggled: null,
|
lastToggled: null,
|
||||||
selectedState: {},
|
selectedState: {},
|
||||||
isConfirmRemoveModalOpen: false,
|
isConfirmRemoveModalOpen: false,
|
||||||
|
isConfirmClearModalOpen: false,
|
||||||
items: props.items
|
items: props.items
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -90,6 +91,19 @@ class Blocklist extends Component {
|
|||||||
this.setState({ isConfirmRemoveModalOpen: false });
|
this.setState({ isConfirmRemoveModalOpen: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onClearBlocklistPress = () => {
|
||||||
|
this.setState({ isConfirmClearModalOpen: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onClearBlocklistConfirmed = () => {
|
||||||
|
this.props.onClearBlocklistPress();
|
||||||
|
this.setState({ isConfirmClearModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
|
onConfirmClearModalClose = () => {
|
||||||
|
this.setState({ isConfirmClearModalOpen: false });
|
||||||
|
};
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
@@ -103,7 +117,6 @@ class Blocklist extends Component {
|
|||||||
totalRecords,
|
totalRecords,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isClearingBlocklistExecuting,
|
isClearingBlocklistExecuting,
|
||||||
onClearBlocklistPress,
|
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -111,7 +124,8 @@ class Blocklist extends Component {
|
|||||||
allSelected,
|
allSelected,
|
||||||
allUnselected,
|
allUnselected,
|
||||||
selectedState,
|
selectedState,
|
||||||
isConfirmRemoveModalOpen
|
isConfirmRemoveModalOpen,
|
||||||
|
isConfirmClearModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const selectedIds = this.getSelectedIds();
|
const selectedIds = this.getSelectedIds();
|
||||||
@@ -131,8 +145,9 @@ class Blocklist extends Component {
|
|||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label={translate('Clear')}
|
label={translate('Clear')}
|
||||||
iconName={icons.CLEAR}
|
iconName={icons.CLEAR}
|
||||||
|
isDisabled={!items.length}
|
||||||
isSpinning={isClearingBlocklistExecuting}
|
isSpinning={isClearingBlocklistExecuting}
|
||||||
onPress={onClearBlocklistPress}
|
onPress={this.onClearBlocklistPress}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
@@ -158,7 +173,7 @@ class Blocklist extends Component {
|
|||||||
{
|
{
|
||||||
!isFetching && !!error &&
|
!isFetching && !!error &&
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToLoadBlocklist')}
|
{translate('BlocklistLoadError')}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,11 +225,21 @@ class Blocklist extends Component {
|
|||||||
isOpen={isConfirmRemoveModalOpen}
|
isOpen={isConfirmRemoveModalOpen}
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
title={translate('RemoveSelected')}
|
title={translate('RemoveSelected')}
|
||||||
message={translate('RemoveSelectedItemBlocklistMessageText')}
|
message={translate('RemoveSelectedBlocklistMessageText')}
|
||||||
confirmLabel={translate('RemoveSelected')}
|
confirmLabel={translate('RemoveSelected')}
|
||||||
onConfirm={this.onRemoveSelectedConfirmed}
|
onConfirm={this.onRemoveSelectedConfirmed}
|
||||||
onCancel={this.onConfirmRemoveModalClose}
|
onCancel={this.onConfirmRemoveModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmClearModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title={translate('ClearBlocklist')}
|
||||||
|
message={translate('ClearBlocklistMessageText')}
|
||||||
|
confirmLabel={translate('Clear')}
|
||||||
|
onConfirm={this.onClearBlocklistConfirmed}
|
||||||
|
onCancel={this.onConfirmClearModalClose}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ class BlocklistRow extends Component {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'movies.sortTitle') {
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<MovieTitleLink
|
<MovieTitleLink
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionList
|
|||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryDetails.css';
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
@@ -24,10 +25,11 @@ function HistoryDetails(props) {
|
|||||||
const {
|
const {
|
||||||
indexer,
|
indexer,
|
||||||
releaseGroup,
|
releaseGroup,
|
||||||
|
movieMatchType,
|
||||||
|
customFormatScore,
|
||||||
nzbInfoUrl,
|
nzbInfoUrl,
|
||||||
downloadClient,
|
downloadClient,
|
||||||
downloadClientName,
|
downloadClientName,
|
||||||
movieMatchType,
|
|
||||||
age,
|
age,
|
||||||
ageHours,
|
ageHours,
|
||||||
ageMinutes,
|
ageMinutes,
|
||||||
@@ -64,16 +66,11 @@ function HistoryDetails(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
nzbInfoUrl ?
|
customFormatScore && customFormatScore !== '0' ?
|
||||||
<span>
|
<DescriptionListItem
|
||||||
<DescriptionListItemTitle>
|
title={translate('CustomFormatScore')}
|
||||||
Info URL
|
data={formatCustomFormatScore(customFormatScore)}
|
||||||
</DescriptionListItemTitle>
|
/> :
|
||||||
|
|
||||||
<DescriptionListItemDescription>
|
|
||||||
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
</span> :
|
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,6 +84,20 @@ function HistoryDetails(props) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
nzbInfoUrl ?
|
||||||
|
<span>
|
||||||
|
<DescriptionListItemTitle>
|
||||||
|
{translate('InfoUrl')}
|
||||||
|
</DescriptionListItemTitle>
|
||||||
|
|
||||||
|
<DescriptionListItemDescription>
|
||||||
|
<Link to={nzbInfoUrl}>{nzbInfoUrl}</Link>
|
||||||
|
</DescriptionListItemDescription>
|
||||||
|
</span> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
downloadClientNameInfo ?
|
downloadClientNameInfo ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
@@ -99,7 +110,7 @@ function HistoryDetails(props) {
|
|||||||
{
|
{
|
||||||
downloadId ?
|
downloadId ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('GrabID')}
|
title={translate('GrabId')}
|
||||||
data={downloadId}
|
data={downloadId}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
@@ -142,7 +153,7 @@ function HistoryDetails(props) {
|
|||||||
{
|
{
|
||||||
downloadId ?
|
downloadId ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('GrabID')}
|
title={translate('GrabId')}
|
||||||
data={downloadId}
|
data={downloadId}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
@@ -162,6 +173,7 @@ function HistoryDetails(props) {
|
|||||||
|
|
||||||
if (eventType === 'downloadFolderImported') {
|
if (eventType === 'downloadFolderImported') {
|
||||||
const {
|
const {
|
||||||
|
customFormatScore,
|
||||||
droppedPath,
|
droppedPath,
|
||||||
importedPath
|
importedPath
|
||||||
} = data;
|
} = data;
|
||||||
@@ -193,26 +205,36 @@ function HistoryDetails(props) {
|
|||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
customFormatScore && customFormatScore !== '0' ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(customFormatScore)}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'movieFileDeleted') {
|
if (eventType === 'movieFileDeleted') {
|
||||||
const {
|
const {
|
||||||
reason
|
reason,
|
||||||
|
customFormatScore
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
let reasonMessage = '';
|
let reasonMessage = '';
|
||||||
|
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'Manual':
|
case 'Manual':
|
||||||
reasonMessage = translate('FileWasDeletedByViaUI');
|
reasonMessage = translate('DeletedReasonManual');
|
||||||
break;
|
break;
|
||||||
case 'MissingFromDisk':
|
case 'MissingFromDisk':
|
||||||
reasonMessage = translate('MissingFromDisk');
|
reasonMessage = translate('DeletedReasonMissingFromDisk');
|
||||||
break;
|
break;
|
||||||
case 'Upgrade':
|
case 'Upgrade':
|
||||||
reasonMessage = translate('FileWasDeletedByUpgrade');
|
reasonMessage = translate('DeletedReasonUpgrade');
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
reasonMessage = '';
|
reasonMessage = '';
|
||||||
@@ -229,6 +251,15 @@ function HistoryDetails(props) {
|
|||||||
title={translate('Reason')}
|
title={translate('Reason')}
|
||||||
data={reasonMessage}
|
data={reasonMessage}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
customFormatScore && customFormatScore !== '0' ?
|
||||||
|
<DescriptionListItem
|
||||||
|
title={translate('CustomFormatScore')}
|
||||||
|
data={formatCustomFormatScore(customFormatScore)}
|
||||||
|
/> :
|
||||||
|
null
|
||||||
|
}
|
||||||
</DescriptionList>
|
</DescriptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -282,7 +313,7 @@ function HistoryDetails(props) {
|
|||||||
{
|
{
|
||||||
downloadId ?
|
downloadId ?
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
title={translate('GrabID')}
|
title={translate('GrabId')}
|
||||||
data={downloadId}
|
data={downloadId}
|
||||||
/> :
|
/> :
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -15,19 +15,19 @@ import styles from './HistoryDetailsModal.css';
|
|||||||
function getHeaderTitle(eventType) {
|
function getHeaderTitle(eventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return 'Grabbed';
|
return translate('Grabbed');
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return 'Download Failed';
|
return translate('DownloadFailed');
|
||||||
case 'downloadFolderImported':
|
case 'downloadFolderImported':
|
||||||
return 'Movie Imported';
|
return translate('MovieImported');
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return 'Movie File Deleted';
|
return translate('MovieFileDeleted');
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return 'Movie File Renamed';
|
return translate('MovieFileRenamed');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
return 'Download Ignored';
|
return translate('DownloadIgnored');
|
||||||
default:
|
default:
|
||||||
return 'Unknown';
|
return translate('Unknown');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
|||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
import HistoryRowConnector from './HistoryRowConnector';
|
||||||
|
|
||||||
class History extends Component {
|
class History extends Component {
|
||||||
@@ -33,6 +34,7 @@ class History extends Component {
|
|||||||
columns,
|
columns,
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
customFilters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
onFirstPagePress,
|
onFirstPagePress,
|
||||||
@@ -70,7 +72,8 @@ class History extends Component {
|
|||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={[]}
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
@@ -85,7 +88,7 @@ class History extends Component {
|
|||||||
{
|
{
|
||||||
!isFetchingAny && hasError &&
|
!isFetchingAny && hasError &&
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('UnableToLoadHistory')}
|
{translate('HistoryLoadError')}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +98,7 @@ class History extends Component {
|
|||||||
|
|
||||||
isPopulated && !hasError && !items.length &&
|
isPopulated && !hasError && !items.length &&
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('NoHistory')}
|
{translate('NoHistoryFound')}
|
||||||
</Alert>
|
</Alert>
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +147,9 @@ History.propTypes = {
|
|||||||
moviesError: PropTypes.object,
|
moviesError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onFirstPagePress: PropTypes.func.isRequired
|
onFirstPagePress: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
import * as historyActions from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import History from './History';
|
import History from './History';
|
||||||
|
|
||||||
@@ -11,11 +12,13 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.history,
|
(state) => state.history,
|
||||||
(state) => state.movies,
|
(state) => state.movies,
|
||||||
(history, movies) => {
|
createCustomFiltersSelector('history'),
|
||||||
|
(history, movies, customFilters) => {
|
||||||
return {
|
return {
|
||||||
isMoviesFetching: movies.isFetching,
|
isMoviesFetching: movies.isFetching,
|
||||||
isMoviesPopulated: movies.isPopulated,
|
isMoviesPopulated: movies.isPopulated,
|
||||||
moviesError: movies.error,
|
moviesError: movies.error,
|
||||||
|
customFilters,
|
||||||
...history
|
...history
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './HistoryEventTypeCell.css';
|
import styles from './HistoryEventTypeCell.css';
|
||||||
|
|
||||||
function getIconName(eventType) {
|
function getIconName(eventType) {
|
||||||
@@ -38,21 +39,21 @@ function getIconKind(eventType) {
|
|||||||
function getTooltip(eventType, data) {
|
function getTooltip(eventType, data) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
case 'grabbed':
|
case 'grabbed':
|
||||||
return `Movie grabbed from ${data.indexer} and sent to ${data.downloadClient}`;
|
return translate('MovieGrabbedHistoryTooltip', { indexer: data.indexer, downloadClient: data.downloadClient });
|
||||||
case 'movieFolderImported':
|
case 'movieFolderImported':
|
||||||
return 'Movie imported from movie folder';
|
return translate('MovieFolderImportedTooltip');
|
||||||
case 'downloadFolderImported':
|
case 'downloadFolderImported':
|
||||||
return 'Movie downloaded successfully and picked up from download client';
|
return translate('MovieImportedTooltip');
|
||||||
case 'downloadFailed':
|
case 'downloadFailed':
|
||||||
return 'Movie download failed';
|
return translate('MovieDownloadFailedTooltip');
|
||||||
case 'movieFileDeleted':
|
case 'movieFileDeleted':
|
||||||
return 'Movie file deleted';
|
return translate('MovieFileDeletedTooltip');
|
||||||
case 'movieFileRenamed':
|
case 'movieFileRenamed':
|
||||||
return 'Movie file renamed';
|
return translate('MovieFileRenamedTooltip');
|
||||||
case 'downloadIgnored':
|
case 'downloadIgnored':
|
||||||
return 'Movie Download Ignored';
|
return translate('MovieDownloadIgnoredTooltip');
|
||||||
default:
|
default:
|
||||||
return 'Unknown event';
|
return translate('UnknownEventTooltip');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
54
frontend/src/Activity/History/HistoryFilterModal.tsx
Normal file
54
frontend/src/Activity/History/HistoryFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||||
|
|
||||||
|
function createHistorySelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createHistorySelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'history';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setHistoryFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ class HistoryRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'movies.sortTitle') {
|
if (name === 'movieMetadata.sortTitle') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<MovieTitleLink
|
<MovieTitleLink
|
||||||
@@ -217,10 +217,12 @@ class HistoryRow extends Component {
|
|||||||
key={name}
|
key={name}
|
||||||
className={styles.details}
|
className={styles.details}
|
||||||
>
|
>
|
||||||
<IconButton
|
<div className={styles.actionContents}>
|
||||||
name={icons.INFO}
|
<IconButton
|
||||||
onPress={this.onDetailsPress}
|
name={icons.INFO}
|
||||||
/>
|
onPress={this.onDetailsPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
@@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
|||||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
|
import QueueFilterModal from './QueueFilterModal';
|
||||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||||
@@ -153,11 +155,16 @@ class Queue extends Component {
|
|||||||
isMoviesPopulated,
|
isMoviesPopulated,
|
||||||
moviesError,
|
moviesError,
|
||||||
columns,
|
columns,
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
count,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isGrabbing,
|
isGrabbing,
|
||||||
isRemoving,
|
isRemoving,
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
|
onFilterSelect,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -220,6 +227,15 @@ class Queue extends Component {
|
|||||||
iconName={icons.TABLE}
|
iconName={icons.TABLE}
|
||||||
/>
|
/>
|
||||||
</TableOptionsModalWrapper>
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={QueueFilterModal}
|
||||||
|
onFilterSelect={onFilterSelect}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
@@ -233,7 +249,7 @@ class Queue extends Component {
|
|||||||
{
|
{
|
||||||
!isRefreshing && hasError ?
|
!isRefreshing && hasError ?
|
||||||
<Alert kind={kinds.DANGER}>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('FailedToLoadQueue')}
|
{translate('QueueLoadError')}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -241,7 +257,11 @@ class Queue extends Component {
|
|||||||
{
|
{
|
||||||
isAllPopulated && !hasError && !items.length ?
|
isAllPopulated && !hasError && !items.length ?
|
||||||
<Alert kind={kinds.INFO}>
|
<Alert kind={kinds.INFO}>
|
||||||
{translate('QueueIsEmpty')}
|
{
|
||||||
|
selectedFilterKey !== 'all' && count > 0 ?
|
||||||
|
translate('QueueFilterHasNoItems') :
|
||||||
|
translate('QueueIsEmpty')
|
||||||
|
}
|
||||||
</Alert> :
|
</Alert> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -325,13 +345,22 @@ Queue.propTypes = {
|
|||||||
moviesError: PropTypes.object,
|
moviesError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
count: PropTypes.number.isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
isRemoving: PropTypes.bool.isRequired,
|
isRemoving: PropTypes.bool.isRequired,
|
||||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||||
onRefreshPress: PropTypes.func.isRequired,
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
onRemoveSelectedPress: PropTypes.func.isRequired,
|
||||||
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
Queue.defaultProps = {
|
||||||
|
count: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Queue;
|
export default Queue;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
|
|||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as queueActions from 'Store/Actions/queueActions';
|
import * as queueActions from 'Store/Actions/queueActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import Queue from './Queue';
|
import Queue from './Queue';
|
||||||
@@ -15,12 +16,16 @@ function createMapStateToProps() {
|
|||||||
(state) => state.movies,
|
(state) => state.movies,
|
||||||
(state) => state.queue.options,
|
(state) => state.queue.options,
|
||||||
(state) => state.queue.paged,
|
(state) => state.queue.paged,
|
||||||
|
(state) => state.queue.status.item,
|
||||||
|
createCustomFiltersSelector('queue'),
|
||||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||||
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
(movies, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => {
|
||||||
return {
|
return {
|
||||||
|
count: options.includeUnknownMovieItems ? status.totalCount : status.count,
|
||||||
isMoviesFetching: movies.isFetching,
|
isMoviesFetching: movies.isFetching,
|
||||||
isMoviesPopulated: movies.isPopulated,
|
isMoviesPopulated: movies.isPopulated,
|
||||||
moviesError: movies.error,
|
moviesError: movies.error,
|
||||||
|
customFilters,
|
||||||
isRefreshMonitoredDownloadsExecuting,
|
isRefreshMonitoredDownloadsExecuting,
|
||||||
...options,
|
...options,
|
||||||
...queue
|
...queue
|
||||||
@@ -106,6 +111,10 @@ class QueueConnector extends Component {
|
|||||||
this.props.setQueueSort({ sortKey });
|
this.props.setQueueSort({ sortKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onFilterSelect = (selectedFilterKey) => {
|
||||||
|
this.props.setQueueFilter({ selectedFilterKey });
|
||||||
|
};
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
onTableOptionChange = (payload) => {
|
||||||
this.props.setQueueTableOption(payload);
|
this.props.setQueueTableOption(payload);
|
||||||
|
|
||||||
@@ -140,6 +149,7 @@ class QueueConnector extends Component {
|
|||||||
onLastPagePress={this.onLastPagePress}
|
onLastPagePress={this.onLastPagePress}
|
||||||
onPageSelect={this.onPageSelect}
|
onPageSelect={this.onPageSelect}
|
||||||
onSortPress={this.onSortPress}
|
onSortPress={this.onSortPress}
|
||||||
|
onFilterSelect={this.onFilterSelect}
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
onRefreshPress={this.onRefreshPress}
|
onRefreshPress={this.onRefreshPress}
|
||||||
onGrabSelectedPress={this.onGrabSelectedPress}
|
onGrabSelectedPress={this.onGrabSelectedPress}
|
||||||
@@ -162,6 +172,7 @@ QueueConnector.propTypes = {
|
|||||||
gotoQueueLastPage: PropTypes.func.isRequired,
|
gotoQueueLastPage: PropTypes.func.isRequired,
|
||||||
gotoQueuePage: PropTypes.func.isRequired,
|
gotoQueuePage: PropTypes.func.isRequired,
|
||||||
setQueueSort: PropTypes.func.isRequired,
|
setQueueSort: PropTypes.func.isRequired,
|
||||||
|
setQueueFilter: PropTypes.func.isRequired,
|
||||||
setQueueTableOption: PropTypes.func.isRequired,
|
setQueueTableOption: PropTypes.func.isRequired,
|
||||||
clearQueue: PropTypes.func.isRequired,
|
clearQueue: PropTypes.func.isRequired,
|
||||||
grabQueueItems: PropTypes.func.isRequired,
|
grabQueueItems: PropTypes.func.isRequired,
|
||||||
|
|||||||
5
frontend/src/Activity/Queue/QueueDetails.css
Normal file
5
frontend/src/Activity/Queue/QueueDetails.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.progressBarContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
7
frontend/src/Activity/Queue/QueueDetails.css.d.ts
vendored
Normal file
7
frontend/src/Activity/Queue/QueueDetails.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'progressBarContainer': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
@@ -1,116 +1,71 @@
|
|||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import QueueStatus from './QueueStatus';
|
||||||
|
import styles from './QueueDetails.css';
|
||||||
|
|
||||||
function QueueDetails(props) {
|
function QueueDetails(props) {
|
||||||
const {
|
const {
|
||||||
title,
|
title,
|
||||||
size,
|
size,
|
||||||
sizeleft,
|
sizeleft,
|
||||||
estimatedCompletionTime,
|
|
||||||
status,
|
status,
|
||||||
trackedDownloadState,
|
trackedDownloadState,
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus,
|
||||||
|
statusMessages,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
progressBar
|
progressBar
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||||
|
const isDownloading = status === 'downloading';
|
||||||
|
const isPaused = status === 'paused';
|
||||||
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
|
const hasError = trackedDownloadStatus === 'error';
|
||||||
|
|
||||||
if (status === 'pending') {
|
if (
|
||||||
return (
|
(isDownloading || isPaused) &&
|
||||||
<Icon
|
!hasWarning &&
|
||||||
name={icons.PENDING}
|
!hasError
|
||||||
title={translate('ReleaseWillBeProcessedInterp', [moment(estimatedCompletionTime).fromNow()])}
|
) {
|
||||||
/>
|
const state = isPaused ? translate('Paused') : translate('Downloading');
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'completed') {
|
if (progress < 5) {
|
||||||
if (errorMessage) {
|
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.DOWNLOAD}
|
name={icons.DOWNLOADING}
|
||||||
kind={kinds.DANGER}
|
title={`${state} - ${progress.toFixed(1)}% ${title}`}
|
||||||
title={translate('ImportFailedInterp', { errorMessage })}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackedDownloadStatus === 'warning') {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
name={icons.DOWNLOAD}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('UnableToImportCheckLogs')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<Icon
|
<Popover
|
||||||
name={icons.DOWNLOADING}
|
className={styles.progressBarContainer}
|
||||||
kind={kinds.DANGER}
|
anchor={progressBar}
|
||||||
title={translate('DownloadFailedInterp', { errorMessage })}
|
title={`${state} - ${progress.toFixed(1)}%`}
|
||||||
|
body={
|
||||||
|
<div>{title}</div>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'failed') {
|
return (
|
||||||
return (
|
<QueueStatus
|
||||||
<Icon
|
sourceTitle={title}
|
||||||
name={icons.DOWNLOADING}
|
status={status}
|
||||||
kind={kinds.DANGER}
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
title={translate('DownloadFailedCheckDownloadClientForMoreDetails')}
|
trackedDownloadState={trackedDownloadState}
|
||||||
/>
|
statusMessages={statusMessages}
|
||||||
);
|
errorMessage={errorMessage}
|
||||||
}
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
if (status === 'warning') {
|
);
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title={translate('DownloadWarningCheckDownloadClientForMoreDetails')}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (progress < 5) {
|
|
||||||
return (
|
|
||||||
<Icon
|
|
||||||
name={icons.DOWNLOADING}
|
|
||||||
title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return progressBar;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QueueDetails.propTypes = {
|
QueueDetails.propTypes = {
|
||||||
@@ -121,8 +76,14 @@ QueueDetails.propTypes = {
|
|||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
trackedDownloadState: PropTypes.string.isRequired,
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||||
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
progressBar: PropTypes.node.isRequired
|
progressBar: PropTypes.node.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
QueueDetails.defaultProps = {
|
||||||
|
trackedDownloadStatus: 'ok',
|
||||||
|
trackedDownloadState: 'downloading'
|
||||||
|
};
|
||||||
|
|
||||||
export default QueueDetails;
|
export default QueueDetails;
|
||||||
|
|||||||
54
frontend/src/Activity/Queue/QueueFilterModal.tsx
Normal file
54
frontend/src/Activity/Queue/QueueFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setQueueFilter } from 'Store/Actions/queueActions';
|
||||||
|
|
||||||
|
function createQueueSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.queue.paged.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function QueueFilterModal(props: QueueFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createQueueSelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'queue';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setQueueFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ class QueueOptions extends Component {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="includeUnknownMovieItems"
|
name="includeUnknownMovieItems"
|
||||||
value={includeUnknownMovieItems}
|
value={includeUnknownMovieItems}
|
||||||
helpText={translate('IncludeUnknownMovieItemsHelpText')}
|
helpText={translate('ShowUnknownMovieItemsHelpText')}
|
||||||
onChange={this.onOptionChange}
|
onChange={this.onOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
3
frontend/src/Activity/Queue/QueueStatus.css
Normal file
3
frontend/src/Activity/Queue/QueueStatus.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.noMessages {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
7
frontend/src/Activity/Queue/QueueStatus.css.d.ts
vendored
Normal file
7
frontend/src/Activity/Queue/QueueStatus.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// This file is automatically generated.
|
||||||
|
// Please do not change this file!
|
||||||
|
interface CssExports {
|
||||||
|
'noMessages': string;
|
||||||
|
}
|
||||||
|
export const cssExports: CssExports;
|
||||||
|
export default cssExports;
|
||||||
162
frontend/src/Activity/Queue/QueueStatus.js
Normal file
162
frontend/src/Activity/Queue/QueueStatus.js
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './QueueStatus.css';
|
||||||
|
|
||||||
|
function getDetailedPopoverBody(statusMessages) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
statusMessages.map(({ title, messages }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={title}
|
||||||
|
className={messages.length ? undefined: styles.noMessages}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
messages.map((message) => {
|
||||||
|
return (
|
||||||
|
<li key={message}>
|
||||||
|
{message}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueStatus(props) {
|
||||||
|
const {
|
||||||
|
sourceTitle,
|
||||||
|
status,
|
||||||
|
trackedDownloadStatus,
|
||||||
|
trackedDownloadState,
|
||||||
|
statusMessages,
|
||||||
|
errorMessage,
|
||||||
|
position,
|
||||||
|
canFlip
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const hasWarning = trackedDownloadStatus === 'warning';
|
||||||
|
const hasError = trackedDownloadStatus === 'error';
|
||||||
|
|
||||||
|
// status === 'downloading'
|
||||||
|
let iconName = icons.DOWNLOADING;
|
||||||
|
let iconKind = kinds.DEFAULT;
|
||||||
|
let title = translate('Downloading');
|
||||||
|
|
||||||
|
if (status === 'paused') {
|
||||||
|
iconName = icons.PAUSED;
|
||||||
|
title = translate('Paused');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'queued') {
|
||||||
|
iconName = icons.QUEUED;
|
||||||
|
title = translate('Queued');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'completed') {
|
||||||
|
iconName = icons.DOWNLOADED;
|
||||||
|
title = translate('Downloaded');
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importPending') {
|
||||||
|
title += ` - ${translate('WaitingToImport')}`;
|
||||||
|
iconKind = kinds.PURPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'importing') {
|
||||||
|
title += ` - ${translate('Importing')}`;
|
||||||
|
iconKind = kinds.PURPLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackedDownloadState === 'failedPending') {
|
||||||
|
title += ` - ${translate('WaitingToProcess')}`;
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasWarning) {
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'delay') {
|
||||||
|
iconName = icons.PENDING;
|
||||||
|
title = translate('Pending');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'downloadClientUnavailable') {
|
||||||
|
iconName = icons.PENDING;
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
title = translate('PendingDownloadClientUnavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'failed') {
|
||||||
|
iconName = icons.DOWNLOADING;
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
title = translate('DownloadFailed');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'warning') {
|
||||||
|
iconName = icons.DOWNLOADING;
|
||||||
|
iconKind = kinds.WARNING;
|
||||||
|
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
||||||
|
title = translate('DownloadWarning', { warningMessage });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
if (status === 'completed') {
|
||||||
|
iconName = icons.DOWNLOAD;
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
title = translate('ImportFailed', { sourceTitle });
|
||||||
|
} else {
|
||||||
|
iconName = icons.DOWNLOADING;
|
||||||
|
iconKind = kinds.DANGER;
|
||||||
|
title = translate('DownloadFailed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<Icon
|
||||||
|
name={iconName}
|
||||||
|
kind={iconKind}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title={title}
|
||||||
|
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
||||||
|
position={position}
|
||||||
|
canFlip={canFlip}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueStatus.propTypes = {
|
||||||
|
sourceTitle: PropTypes.string.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||||
|
trackedDownloadState: PropTypes.string.isRequired,
|
||||||
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
errorMessage: PropTypes.string,
|
||||||
|
position: PropTypes.oneOf(tooltipPositions.all).isRequired,
|
||||||
|
canFlip: PropTypes.bool.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
QueueStatus.defaultProps = {
|
||||||
|
trackedDownloadStatus: 'ok',
|
||||||
|
trackedDownloadState: 'downloading',
|
||||||
|
canFlip: false
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QueueStatus;
|
||||||
@@ -1,39 +1,10 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import { tooltipPositions } from 'Helpers/Props';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import QueueStatus from './QueueStatus';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
import styles from './QueueStatusCell.css';
|
import styles from './QueueStatusCell.css';
|
||||||
|
|
||||||
function getDetailedPopoverBody(statusMessages) {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
statusMessages.map(({ title, messages }) => {
|
|
||||||
return (
|
|
||||||
<div key={title}>
|
|
||||||
{title}
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
messages.map((message) => {
|
|
||||||
return (
|
|
||||||
<li key={message}>
|
|
||||||
{message}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function QueueStatusCell(props) {
|
function QueueStatusCell(props) {
|
||||||
const {
|
const {
|
||||||
sourceTitle,
|
sourceTitle,
|
||||||
@@ -44,97 +15,16 @@ function QueueStatusCell(props) {
|
|||||||
errorMessage
|
errorMessage
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const hasWarning = trackedDownloadStatus === 'warning';
|
|
||||||
const hasError = trackedDownloadStatus === 'error';
|
|
||||||
|
|
||||||
// status === 'downloading'
|
|
||||||
let iconName = icons.DOWNLOADING;
|
|
||||||
let iconKind = kinds.DEFAULT;
|
|
||||||
let title = translate('Downloading');
|
|
||||||
|
|
||||||
if (status === 'paused') {
|
|
||||||
iconName = icons.PAUSED;
|
|
||||||
title = translate('Paused');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'queued') {
|
|
||||||
iconName = icons.QUEUED;
|
|
||||||
title = translate('Queued');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'completed') {
|
|
||||||
iconName = icons.DOWNLOADED;
|
|
||||||
title = translate('Downloaded');
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importPending') {
|
|
||||||
title += ` - ${translate('WaitingToImport')}`;
|
|
||||||
iconKind = kinds.PURPLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'importing') {
|
|
||||||
title += ` - ${translate('Importing')}`;
|
|
||||||
iconKind = kinds.PURPLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackedDownloadState === 'failedPending') {
|
|
||||||
title += ` - ${translate('WaitingToProcess')}`;
|
|
||||||
iconKind = kinds.DANGER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasWarning) {
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'delay') {
|
|
||||||
iconName = icons.PENDING;
|
|
||||||
title = translate('Pending');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'DownloadClientUnavailable') {
|
|
||||||
iconName = icons.PENDING;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
title = `${translate('Pending')} - ${translate('DownloadClientUnavailable')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'failed') {
|
|
||||||
iconName = icons.DOWNLOADING;
|
|
||||||
iconKind = kinds.DANGER;
|
|
||||||
title = translate('DownloadFailed');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status === 'warning') {
|
|
||||||
iconName = icons.DOWNLOADING;
|
|
||||||
iconKind = kinds.WARNING;
|
|
||||||
const warningMessage = errorMessage || translate('CheckDownloadClientForDetails');
|
|
||||||
title = translate('DownloadWarning', { warningMessage });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasError) {
|
|
||||||
if (status === 'completed') {
|
|
||||||
iconName = icons.DOWNLOAD;
|
|
||||||
iconKind = kinds.DANGER;
|
|
||||||
title = translate('ImportFailed', { sourceTitle });
|
|
||||||
} else {
|
|
||||||
iconName = icons.DOWNLOADING;
|
|
||||||
iconKind = kinds.DANGER;
|
|
||||||
title = translate('DownloadFailed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell className={styles.status}>
|
<TableRowCell className={styles.status}>
|
||||||
<Popover
|
<QueueStatus
|
||||||
anchor={
|
sourceTitle={sourceTitle}
|
||||||
<Icon
|
status={status}
|
||||||
name={iconName}
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
kind={iconKind}
|
trackedDownloadState={trackedDownloadState}
|
||||||
/>
|
statusMessages={statusMessages}
|
||||||
}
|
errorMessage={errorMessage}
|
||||||
title={title}
|
|
||||||
body={hasWarning || hasError ? getDetailedPopoverBody(statusMessages) : sourceTitle}
|
|
||||||
position={tooltipPositions.RIGHT}
|
position={tooltipPositions.RIGHT}
|
||||||
canFlip={false}
|
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
@@ -150,8 +40,8 @@ QueueStatusCell.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
QueueStatusCell.defaultProps = {
|
QueueStatusCell.defaultProps = {
|
||||||
trackedDownloadStatus: translate('Ok'),
|
trackedDownloadStatus: 'ok',
|
||||||
trackedDownloadState: translate('Downloading')
|
trackedDownloadState: 'downloading'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default QueueStatusCell;
|
export default QueueStatusCell;
|
||||||
|
|||||||
@@ -115,11 +115,12 @@ class RemoveQueueItemModal extends Component {
|
|||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="blocklist"
|
name="blocklist"
|
||||||
value={blocklist}
|
value={blocklist}
|
||||||
helpText={translate('BlocklistHelpText')}
|
helpText={translate('BlocklistReleaseHelpText')}
|
||||||
onChange={this.onBlocklistChange}
|
onChange={this.onBlocklistChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
@@ -149,7 +150,7 @@ class RemoveQueueItemModal extends Component {
|
|||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
onPress={this.onRemoveConfirmed}
|
onPress={this.onRemoveConfirmed}
|
||||||
>
|
>
|
||||||
Remove
|
{translate('Remove')}
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class RemoveQueueItemsModal extends Component {
|
|||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="blocklist"
|
name="blocklist"
|
||||||
value={blocklist}
|
value={blocklist}
|
||||||
helpText={translate('BlocklistHelpText')}
|
helpText={translate('BlocklistReleaseHelpText')}
|
||||||
onChange={this.onBlocklistChange}
|
onChange={this.onBlocklistChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import formatTime from 'Utilities/Date/formatTime';
|
import formatTime from 'Utilities/Date/formatTime';
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
@@ -25,11 +28,13 @@ function TimeleftCell(props) {
|
|||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.timeleft}>
|
||||||
className={styles.timeleft}
|
<Tooltip
|
||||||
title={translate('DelayingDownloadUntilInterp', [date, time])}
|
anchor={<Icon name={icons.INFO} />}
|
||||||
>
|
tooltip={translate('DelayingDownloadUntil', { date, time })}
|
||||||
-
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -39,11 +44,13 @@ function TimeleftCell(props) {
|
|||||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell className={styles.timeleft}>
|
||||||
className={styles.timeleft}
|
<Tooltip
|
||||||
title={translate('RetryingDownloadInterp', [date, time])}
|
anchor={<Icon name={icons.INFO} />}
|
||||||
>
|
tooltip={translate('RetryingDownloadOn', { date, time })}
|
||||||
-
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import TextInput from 'Components/Form/TextInput';
|
import TextInput from 'Components/Form/TextInput';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
@@ -130,7 +131,12 @@ class AddNewMovie extends Component {
|
|||||||
<div className={styles.helpText}>
|
<div className={styles.helpText}>
|
||||||
{translate('FailedLoadingSearchResults')}
|
{translate('FailedLoadingSearchResults')}
|
||||||
</div>
|
</div>
|
||||||
<div>{getErrorMessage(error)}</div>
|
<Alert kind={kinds.WARNING}>{getErrorMessage(error)}</Alert>
|
||||||
|
<div>
|
||||||
|
<Link to="https://wiki.servarr.com/radarr/troubleshooting#invalid-response-received-from-tmdb">
|
||||||
|
{translate('WhySearchesCouldBeFailing')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div> : null
|
</div> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
ratings,
|
ratings,
|
||||||
folder,
|
folder,
|
||||||
images,
|
images,
|
||||||
|
existingMovieId,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isExclusionMovie,
|
isExclusionMovie,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
@@ -74,8 +75,7 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
monitored,
|
monitored,
|
||||||
hasFile,
|
hasFile,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
queueStatus,
|
movieFile,
|
||||||
queueState,
|
|
||||||
runtime,
|
runtime,
|
||||||
movieRuntimeFormat,
|
movieRuntimeFormat,
|
||||||
certification
|
certification
|
||||||
@@ -120,13 +120,13 @@ class AddNewMovieSearchResult extends Component {
|
|||||||
{
|
{
|
||||||
isExistingMovie &&
|
isExistingMovie &&
|
||||||
<MovieIndexProgressBar
|
<MovieIndexProgressBar
|
||||||
|
movieId={existingMovieId}
|
||||||
|
movieFile={movieFile}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
hasFile={hasFile}
|
hasFile={hasFile}
|
||||||
status={status}
|
status={status}
|
||||||
width={posterWidth}
|
width={posterWidth}
|
||||||
detailedProgressBar={true}
|
detailedProgressBar={true}
|
||||||
queueStatus={queueStatus}
|
|
||||||
queueState={queueState}
|
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -278,6 +278,7 @@ AddNewMovieSearchResult.propTypes = {
|
|||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
folder: PropTypes.string.isRequired,
|
folder: PropTypes.string.isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
existingMovieId: PropTypes.number,
|
||||||
isExistingMovie: PropTypes.bool.isRequired,
|
isExistingMovie: PropTypes.bool.isRequired,
|
||||||
isExclusionMovie: PropTypes.bool.isRequired,
|
isExclusionMovie: PropTypes.bool.isRequired,
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
@@ -286,9 +287,8 @@ AddNewMovieSearchResult.propTypes = {
|
|||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
hasFile: PropTypes.bool.isRequired,
|
||||||
isAvailable: PropTypes.bool.isRequired,
|
isAvailable: PropTypes.bool.isRequired,
|
||||||
|
movieFile: PropTypes.object,
|
||||||
colorImpairedMode: PropTypes.bool,
|
colorImpairedMode: PropTypes.bool,
|
||||||
queueStatus: PropTypes.string,
|
|
||||||
queueState: PropTypes.string,
|
|
||||||
runtime: PropTypes.number.isRequired,
|
runtime: PropTypes.number.isRequired,
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||||
certification: PropTypes.string
|
certification: PropTypes.string
|
||||||
|
|||||||
@@ -10,17 +10,15 @@ function createMapStateToProps() {
|
|||||||
createExistingMovieSelector(),
|
createExistingMovieSelector(),
|
||||||
createExclusionMovieSelector(),
|
createExclusionMovieSelector(),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(state) => state.queue.details.items,
|
|
||||||
(state, { internalId }) => internalId,
|
(state, { internalId }) => internalId,
|
||||||
(isExistingMovie, isExclusionMovie, dimensions, queueItems, internalId) => {
|
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||||
const firstQueueItem = queueItems.find((q) => q.movieId === internalId && internalId > 0);
|
(isExistingMovie, isExclusionMovie, dimensions, internalId, movieRuntimeFormat) => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
existingMovieId: internalId,
|
||||||
isExistingMovie,
|
isExistingMovie,
|
||||||
isExclusionMovie,
|
isExclusionMovie,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
queueStatus: firstQueueItem ? firstQueueItem.status : null,
|
movieRuntimeFormat
|
||||||
queueState: firstQueueItem ? firstQueueItem.trackedDownloadState : null
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
import { addRootFolder, deleteRootFolder, fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||||
|
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
|
import ImportMovieSelectFolder from './ImportMovieSelectFolder';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.rootFolders,
|
createRootFoldersSelector(),
|
||||||
createSystemStatusSelector(),
|
createSystemStatusSelector(),
|
||||||
(rootFolders, systemStatus) => {
|
(rootFolders, systemStatus) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.version {
|
.version {
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
font-family: var(--defaultFontFamily);
|
||||||
}
|
}
|
||||||
|
|
||||||
.maintenance {
|
.maintenance {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
@@ -64,20 +65,20 @@ function AppUpdatedModalContent(props) {
|
|||||||
return (
|
return (
|
||||||
<ModalContent onModalClose={onModalClose}>
|
<ModalContent onModalClose={onModalClose}>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
{translate('RadarrUpdated')}
|
{translate('AppUpdated')}
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div dangerouslySetInnerHTML={{ __html: translate('VersionUpdateText', [`<span className=${styles.version}>${version}</span>`]) }} />
|
<div>
|
||||||
|
<InlineMarkdown data={translate('AppUpdatedVersion', { version })} blockClassName={styles.version} />
|
||||||
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !error && !!update &&
|
isPopulated && !error && !!update &&
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
!update.changes &&
|
!update.changes &&
|
||||||
<div className={styles.maintenance}>
|
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
|
||||||
{translate('MaintenanceRelease')}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
|||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div>
|
<div>
|
||||||
{translate('ConnectionLostMessage')}
|
{translate('ConnectionLostToBackend')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.automatic}>
|
<div className={styles.automatic}>
|
||||||
{translate('ConnectionLostAutomaticMessage')}
|
{translate('ConnectionLostReconnect')}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import { FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
@@ -20,6 +21,10 @@ export interface PagedAppSectionState {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionFilterState<T> {
|
||||||
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppSectionSchemaState<T> {
|
export interface AppSectionSchemaState<T> {
|
||||||
isSchemaFetching: boolean;
|
isSchemaFetching: boolean;
|
||||||
isSchemaPopulated: boolean;
|
isSchemaPopulated: boolean;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
|
import HistoryAppState from './HistoryAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
@@ -46,6 +47,7 @@ export interface CustomFilter {
|
|||||||
interface AppState {
|
interface AppState {
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
import Movie from 'Movie/Movie';
|
import Movie from 'Movie/Movie';
|
||||||
import { FilterBuilderProp } from './AppState';
|
|
||||||
|
|
||||||
interface CalendarAppState extends AppSectionState<Movie> {
|
interface CalendarAppState
|
||||||
filterBuilderProps: FilterBuilderProp<Movie>[];
|
extends AppSectionState<Movie>,
|
||||||
}
|
AppSectionFilterState<Movie> {}
|
||||||
|
|
||||||
export default CalendarAppState;
|
export default CalendarAppState;
|
||||||
|
|||||||
10
frontend/src/App/State/HistoryAppState.ts
Normal file
10
frontend/src/App/State/HistoryAppState.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import History from 'typings/History';
|
||||||
|
|
||||||
|
interface HistoryAppState
|
||||||
|
extends AppSectionState<History>,
|
||||||
|
AppSectionFilterState<History> {}
|
||||||
|
|
||||||
|
export default HistoryAppState;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState from 'App/State/AppSectionState';
|
||||||
import MovieCollection from 'typings/MovieCollection';
|
import MovieCollection from 'typings/MovieCollection';
|
||||||
|
|
||||||
type MovieCollectionAppState = AppSectionState<MovieCollection>;
|
interface MovieCollectionAppState extends AppSectionState<MovieCollection> {
|
||||||
|
itemMap: Record<number, number>;
|
||||||
|
}
|
||||||
|
|
||||||
export default MovieCollectionAppState;
|
export default MovieCollectionAppState;
|
||||||
|
|||||||
@@ -1,41 +1,17 @@
|
|||||||
import ModelBase from 'App/ModelBase';
|
import Queue from 'typings/Queue';
|
||||||
import Language from 'Language/Language';
|
import AppSectionState, {
|
||||||
import { QualityModel } from 'Quality/Quality';
|
AppSectionFilterState,
|
||||||
import CustomFormat from 'typings/CustomFormat';
|
AppSectionItemState,
|
||||||
import AppSectionState, { AppSectionItemState, Error } from './AppSectionState';
|
Error,
|
||||||
|
} from './AppSectionState';
|
||||||
export interface StatusMessage {
|
|
||||||
title: string;
|
|
||||||
messages: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Queue extends ModelBase {
|
|
||||||
languages: Language[];
|
|
||||||
quality: QualityModel;
|
|
||||||
customFormats: CustomFormat[];
|
|
||||||
size: number;
|
|
||||||
title: string;
|
|
||||||
sizeleft: number;
|
|
||||||
timeleft: string;
|
|
||||||
estimatedCompletionTime: string;
|
|
||||||
status: string;
|
|
||||||
trackedDownloadStatus: string;
|
|
||||||
trackedDownloadState: string;
|
|
||||||
statusMessages: StatusMessage[];
|
|
||||||
errorMessage: string;
|
|
||||||
downloadId: string;
|
|
||||||
protocol: string;
|
|
||||||
downloadClient: string;
|
|
||||||
outputPath: string;
|
|
||||||
movieHasFile: boolean;
|
|
||||||
movieId?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
export interface QueueDetailsAppState extends AppSectionState<Queue> {
|
||||||
params: unknown;
|
params: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueuePagedAppState extends AppSectionState<Queue> {
|
export interface QueuePagedAppState
|
||||||
|
extends AppSectionState<Queue>,
|
||||||
|
AppSectionFilterState<Queue> {
|
||||||
isGrabbing: boolean;
|
isGrabbing: boolean;
|
||||||
grabError: Error;
|
grabError: Error;
|
||||||
isRemoving: boolean;
|
isRemoving: boolean;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Language from 'Language/Language';
|
|||||||
import DownloadClient from 'typings/DownloadClient';
|
import DownloadClient from 'typings/DownloadClient';
|
||||||
import ImportList from 'typings/ImportList';
|
import ImportList from 'typings/ImportList';
|
||||||
import Indexer from 'typings/Indexer';
|
import Indexer from 'typings/Indexer';
|
||||||
|
import IndexerFlag from 'typings/IndexerFlag';
|
||||||
import Notification from 'typings/Notification';
|
import Notification from 'typings/Notification';
|
||||||
import QualityProfile from 'typings/QualityProfile';
|
import QualityProfile from 'typings/QualityProfile';
|
||||||
import { UiSettings } from 'typings/UiSettings';
|
import { UiSettings } from 'typings/UiSettings';
|
||||||
@@ -35,12 +36,14 @@ export interface QualityProfilesAppState
|
|||||||
extends AppSectionState<QualityProfile>,
|
extends AppSectionState<QualityProfile>,
|
||||||
AppSectionSchemaState<QualityProfile> {}
|
AppSectionSchemaState<QualityProfile> {}
|
||||||
|
|
||||||
|
export type IndexerFlagSettingsAppState = AppSectionState<IndexerFlag>;
|
||||||
export type LanguageSettingsAppState = AppSectionState<Language>;
|
export type LanguageSettingsAppState = AppSectionState<Language>;
|
||||||
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
|
||||||
|
|
||||||
interface SettingsAppState {
|
interface SettingsAppState {
|
||||||
downloadClients: DownloadClientAppState;
|
downloadClients: DownloadClientAppState;
|
||||||
importLists: ImportListAppState;
|
importLists: ImportListAppState;
|
||||||
|
indexerFlags: IndexerFlagSettingsAppState;
|
||||||
indexers: IndexerAppState;
|
indexers: IndexerAppState;
|
||||||
languages: LanguageSettingsAppState;
|
languages: LanguageSettingsAppState;
|
||||||
notifications: NotificationAppState;
|
notifications: NotificationAppState;
|
||||||
|
|||||||
@@ -42,9 +42,9 @@ function Agenda(props) {
|
|||||||
<div className={styles.agenda}>
|
<div className={styles.agenda}>
|
||||||
{
|
{
|
||||||
items.map((item, index) => {
|
items.map((item, index) => {
|
||||||
const momentDate = moment(item.inCinemas);
|
const momentDate = moment(item.sortDate);
|
||||||
const showDate = index === 0 ||
|
const showDate = index === 0 ||
|
||||||
!moment(items[index - 1].inCinemas).isSame(momentDate, 'day');
|
!moment(items[index - 1].sortDate).isSame(momentDate, 'day');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AgendaEventConnector
|
<AgendaEventConnector
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
.event {
|
.event {
|
||||||
display: flex;
|
position: relative;
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-bottom: 1px solid var(--borderColor);
|
border-bottom: 1px solid var(--borderColor);
|
||||||
font-size: $defaultFontSize;
|
}
|
||||||
|
|
||||||
|
.underlay {
|
||||||
|
@add-mixin cover;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--tableRowHoverBackgroundColor);
|
background-color: var(--tableRowHoverBackgroundColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.overlay {
|
||||||
composes: link from '~Calendar/Events/CalendarEvent.css';
|
@add-mixin linkOverlay;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
overflow-x: hidden;
|
||||||
|
font-size: $defaultFontSize;
|
||||||
|
|
||||||
|
&:global(.colorImpaired) {
|
||||||
|
border-left-width: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.eventWrapper {
|
.eventWrapper {
|
||||||
@@ -44,6 +55,8 @@
|
|||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -75,7 +88,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.event {
|
.overlay {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +108,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dateIcon {
|
.releaseIcon {
|
||||||
|
margin-right: 20px;
|
||||||
width: 25px;
|
width: 25px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,19 @@
|
|||||||
interface CssExports {
|
interface CssExports {
|
||||||
'continuing': string;
|
'continuing': string;
|
||||||
'date': string;
|
'date': string;
|
||||||
'dateIcon': string;
|
|
||||||
'downloaded': string;
|
'downloaded': string;
|
||||||
'event': string;
|
'event': string;
|
||||||
'eventWrapper': string;
|
'eventWrapper': string;
|
||||||
'genres': string;
|
'genres': string;
|
||||||
'link': string;
|
|
||||||
'missingMonitored': string;
|
'missingMonitored': string;
|
||||||
'missingUnmonitored': string;
|
'missingUnmonitored': string;
|
||||||
'movieTitle': string;
|
'movieTitle': string;
|
||||||
|
'overlay': string;
|
||||||
'queue': string;
|
'queue': string;
|
||||||
|
'releaseIcon': string;
|
||||||
'statusIcon': string;
|
'statusIcon': string;
|
||||||
'time': string;
|
'time': string;
|
||||||
|
'underlay': string;
|
||||||
'unmonitored': string;
|
'unmonitored': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -87,25 +87,24 @@ class AgendaEvent extends Component {
|
|||||||
const link = `/movie/${titleSlug}`;
|
const link = `/movie/${titleSlug}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={styles.event}>
|
||||||
<Link
|
<Link
|
||||||
className={classNames(
|
className={styles.underlay}
|
||||||
styles.event,
|
|
||||||
styles.link
|
|
||||||
)}
|
|
||||||
to={link}
|
to={link}
|
||||||
>
|
/>
|
||||||
<div className={styles.dateIcon}>
|
|
||||||
|
<div className={styles.overlay}>
|
||||||
|
<div className={styles.date}>
|
||||||
|
{showDate ? startTime.format(longDateFormat) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.releaseIcon}>
|
||||||
<Icon
|
<Icon
|
||||||
name={releaseIcon}
|
name={releaseIcon}
|
||||||
kind={kinds.DEFAULT}
|
kind={kinds.DEFAULT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.date}>
|
|
||||||
{(showDate) ? startTime.format(longDateFormat) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.eventWrapper,
|
styles.eventWrapper,
|
||||||
@@ -143,9 +142,7 @@ class AgendaEvent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
showCutoffUnmetIcon &&
|
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet &&
|
||||||
!!movieFile &&
|
|
||||||
movieFile.qualityCutoffNotMet &&
|
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.MOVIE_FILE}
|
name={icons.MOVIE_FILE}
|
||||||
@@ -154,7 +151,7 @@ class AgendaEvent extends Component {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,13 +23,11 @@ function createFilterBuilderPropsSelector() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SeriesIndexFilterModalProps {
|
interface CalendarFilterModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CalendarFilterModal(
|
export default function CalendarFilterModal(props: CalendarFilterModalProps) {
|
||||||
props: SeriesIndexFilterModalProps
|
|
||||||
) {
|
|
||||||
const sectionItems = useSelector(createCalendarSelector());
|
const sectionItems = useSelector(createCalendarSelector());
|
||||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
const customFilterType = 'calendar';
|
const customFilterType = 'calendar';
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import moment from 'moment';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import * as calendarViews from 'Calendar/calendarViews';
|
|
||||||
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
|
|
||||||
import styles from './CalendarDay.css';
|
|
||||||
|
|
||||||
function CalendarDay(props) {
|
|
||||||
const {
|
|
||||||
date,
|
|
||||||
time,
|
|
||||||
isTodaysDate,
|
|
||||||
events,
|
|
||||||
view,
|
|
||||||
onEventModalOpenToggle
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classNames(
|
|
||||||
styles.day,
|
|
||||||
view === calendarViews.DAY && styles.isSingleDay
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
view === calendarViews.MONTH &&
|
|
||||||
<div className={classNames(
|
|
||||||
styles.dayOfMonth,
|
|
||||||
isTodaysDate && styles.isToday,
|
|
||||||
!moment(date).isSame(moment(time), 'month') && styles.isDifferentMonth
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{moment(date).date()}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
events.map((event) => {
|
|
||||||
return (
|
|
||||||
<CalendarEventConnector
|
|
||||||
key={event.id}
|
|
||||||
movieId={event.id}
|
|
||||||
date={date}
|
|
||||||
{...event}
|
|
||||||
onEventModalOpenToggle={onEventModalOpenToggle}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CalendarDay.propTypes = {
|
|
||||||
date: PropTypes.string.isRequired,
|
|
||||||
time: PropTypes.string.isRequired,
|
|
||||||
isTodaysDate: PropTypes.bool.isRequired,
|
|
||||||
events: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
view: PropTypes.string.isRequired,
|
|
||||||
onEventModalOpenToggle: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CalendarDay;
|
|
||||||
67
frontend/src/Calendar/Day/CalendarDay.tsx
Normal file
67
frontend/src/Calendar/Day/CalendarDay.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import moment from 'moment';
|
||||||
|
import React from 'react';
|
||||||
|
import * as calendarViews from 'Calendar/calendarViews';
|
||||||
|
import CalendarEventConnector from 'Calendar/Events/CalendarEventConnector';
|
||||||
|
import CalendarEvent from 'typings/CalendarEvent';
|
||||||
|
import styles from './CalendarDay.css';
|
||||||
|
|
||||||
|
interface CalendarDayProps {
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
isTodaysDate: boolean;
|
||||||
|
events: CalendarEvent[];
|
||||||
|
view: string;
|
||||||
|
onEventModalOpenToggle(...args: unknown[]): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
function CalendarDay(props: CalendarDayProps) {
|
||||||
|
const { date, time, isTodaysDate, events, view, onEventModalOpenToggle } =
|
||||||
|
props;
|
||||||
|
|
||||||
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (isTodaysDate && view === calendarViews.MONTH && ref.current) {
|
||||||
|
ref.current.scrollIntoView();
|
||||||
|
}
|
||||||
|
}, [time, isTodaysDate, view]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={classNames(
|
||||||
|
styles.day,
|
||||||
|
view === calendarViews.DAY && styles.isSingleDay
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{view === calendarViews.MONTH && (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.dayOfMonth,
|
||||||
|
isTodaysDate && styles.isToday,
|
||||||
|
!moment(date).isSame(moment(time), 'month') &&
|
||||||
|
styles.isDifferentMonth
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{moment(date).date()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
{events.map((event) => {
|
||||||
|
return (
|
||||||
|
<CalendarEventConnector
|
||||||
|
key={event.id}
|
||||||
|
{...event}
|
||||||
|
movieId={event.id}
|
||||||
|
date={date as string}
|
||||||
|
onEventModalOpenToggle={onEventModalOpenToggle}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CalendarDay;
|
||||||
@@ -1,9 +1,22 @@
|
|||||||
|
$fullColorGradient: rgba(244, 245, 246, 0.2);
|
||||||
|
|
||||||
.event {
|
.event {
|
||||||
overflow-x: hidden;
|
position: relative;
|
||||||
margin: 4px 2px;
|
margin: 4px 2px;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-bottom: 1px solid var(--calendarBorderColor);
|
border-bottom: 1px solid var(--calendarBorderColor);
|
||||||
border-left: 4px solid var(--calendarBorderColor);
|
border-left: 4px solid var(--calendarBorderColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.underlay {
|
||||||
|
@add-mixin cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
@add-mixin linkOverlay;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
overflow-x: hidden;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&:global(.colorImpaired) {
|
||||||
@@ -11,18 +24,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
|
||||||
composes: link from '~Components/Link/Link.css';
|
|
||||||
|
|
||||||
display: block;
|
|
||||||
color: var(--defaultColor);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--defaultColor);
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.info,
|
.info,
|
||||||
.movieInfo {
|
.movieInfo {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -44,8 +45,15 @@
|
|||||||
font-size: $defaultFontSize;
|
font-size: $defaultFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statusContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.statusIcon {
|
.statusIcon {
|
||||||
margin-left: 3px;
|
margin-left: 3px;
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -55,35 +63,84 @@
|
|||||||
.downloaded {
|
.downloaded {
|
||||||
border-left-color: var(--successColor) !important;
|
border-left-color: var(--successColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(39, 194, 76, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&:global(.colorImpaired) {
|
||||||
border-left-color: color(var(--successColor), saturation(+15%)) !important;
|
border-left-color: color(#27c24c saturation(+15%)) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue {
|
.queue {
|
||||||
border-left-color: var(--purple) !important;
|
border-left-color: var(--purple) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(122, 67, 182, 0.4) !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.unmonitored {
|
.unmonitored {
|
||||||
border-left-color: var(--gray) !important;
|
border-left-color: var(--gray) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(173, 173, 173, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(45deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingUnmonitored {
|
.missingUnmonitored {
|
||||||
border-left-color: var(--warningColor) !important;
|
border-left-color: var(--warningColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(255, 165, 0, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&:global(.colorImpaired) {
|
||||||
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingMonitored {
|
.missingMonitored {
|
||||||
border-left-color: var(--dangerColor) !important;
|
border-left-color: var(--dangerColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(240, 80, 80, 0.6) !important;
|
||||||
|
}
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&:global(.colorImpaired) {
|
||||||
|
border-left-color: color(#f05050 saturation(+15%)) !important;
|
||||||
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.continuing {
|
.continuing {
|
||||||
border-left-color: var(--primaryColor) !important;
|
border-left-color: var(--primaryColor) !important;
|
||||||
|
|
||||||
|
&:global(.fullColor) {
|
||||||
|
background-color: rgba(93, 156, 236, 0.4) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.fullColor.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(90deg, $fullColorGradient, $fullColorGradient 5px, transparent 5px, transparent 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ interface CssExports {
|
|||||||
'event': string;
|
'event': string;
|
||||||
'genres': string;
|
'genres': string;
|
||||||
'info': string;
|
'info': string;
|
||||||
'link': string;
|
|
||||||
'missingMonitored': string;
|
'missingMonitored': string;
|
||||||
'missingUnmonitored': string;
|
'missingUnmonitored': string;
|
||||||
'movieInfo': string;
|
'movieInfo': string;
|
||||||
'movieTitle': string;
|
'movieTitle': string;
|
||||||
|
'overlay': string;
|
||||||
'queue': string;
|
'queue': string;
|
||||||
|
'statusContainer': string;
|
||||||
'statusIcon': string;
|
'statusIcon': string;
|
||||||
|
'underlay': string;
|
||||||
'unmonitored': string;
|
'unmonitored': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class CalendarEvent extends Component {
|
|||||||
title,
|
title,
|
||||||
titleSlug,
|
titleSlug,
|
||||||
genres,
|
genres,
|
||||||
|
date,
|
||||||
monitored,
|
monitored,
|
||||||
certification,
|
certification,
|
||||||
hasFile,
|
hasFile,
|
||||||
@@ -32,8 +33,8 @@ class CalendarEvent extends Component {
|
|||||||
queueItem,
|
queueItem,
|
||||||
showMovieInformation,
|
showMovieInformation,
|
||||||
showCutoffUnmetIcon,
|
showCutoffUnmetIcon,
|
||||||
colorImpairedMode,
|
fullColorEvents,
|
||||||
date
|
colorImpairedMode
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isDownloading = !!(queueItem || grabbed);
|
const isDownloading = !!(queueItem || grabbed);
|
||||||
@@ -56,64 +57,71 @@ class CalendarEvent extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.event,
|
||||||
|
styles[statusStyle],
|
||||||
|
colorImpairedMode && 'colorImpaired',
|
||||||
|
fullColorEvents && 'fullColor'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Link
|
<Link
|
||||||
className={classNames(
|
className={styles.underlay}
|
||||||
styles.event,
|
|
||||||
styles.link,
|
|
||||||
styles[statusStyle],
|
|
||||||
colorImpairedMode && 'colorImpaired'
|
|
||||||
)}
|
|
||||||
// component="div"
|
|
||||||
to={link}
|
to={link}
|
||||||
>
|
/>
|
||||||
|
|
||||||
|
<div className={styles.overlay} >
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
<div className={styles.movieTitle}>
|
<div className={styles.movieTitle}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
<div className={styles.statusContainer}>
|
||||||
!!queueItem &&
|
{
|
||||||
<span className={styles.statusIcon}>
|
queueItem ?
|
||||||
<CalendarEventQueueDetails
|
<span className={styles.statusIcon}>
|
||||||
{...queueItem}
|
<CalendarEventQueueDetails
|
||||||
/>
|
{...queueItem}
|
||||||
</span>
|
/>
|
||||||
}
|
</span> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!queueItem && grabbed &&
|
!queueItem && grabbed ?
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.statusIcon}
|
className={styles.statusIcon}
|
||||||
name={icons.DOWNLOADING}
|
name={icons.DOWNLOADING}
|
||||||
title={translate('MovieIsDownloading')}
|
title={translate('MovieIsDownloading')}
|
||||||
/>
|
/> :
|
||||||
}
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
showCutoffUnmetIcon &&
|
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet ?
|
||||||
!!movieFile &&
|
<Icon
|
||||||
movieFile.qualityCutoffNotMet &&
|
className={styles.statusIcon}
|
||||||
<Icon
|
name={icons.MOVIE_FILE}
|
||||||
className={styles.statusIcon}
|
kind={kinds.WARNING}
|
||||||
name={icons.MOVIE_FILE}
|
title={translate('QualityCutoffHasNotBeenMet')}
|
||||||
kind={kinds.WARNING}
|
/> :
|
||||||
title={translate('QualityCutoffHasNotBeenMet')}
|
null
|
||||||
/>
|
}
|
||||||
}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
showMovieInformation &&
|
showMovieInformation ?
|
||||||
<div className={styles.movieInfo}>
|
<div className={styles.movieInfo}>
|
||||||
<div className={styles.genres}>
|
<div className={styles.genres}>
|
||||||
{joinedGenres}
|
{joinedGenres}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
showMovieInformation &&
|
showMovieInformation ?
|
||||||
<div className={styles.movieInfo}>
|
<div className={styles.movieInfo}>
|
||||||
<div className={styles.genres}>
|
<div className={styles.genres}>
|
||||||
{eventType.join(', ')}
|
{eventType.join(', ')}
|
||||||
@@ -121,10 +129,10 @@ class CalendarEvent extends Component {
|
|||||||
<div>
|
<div>
|
||||||
{certification}
|
{certification}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</Link>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -140,16 +148,18 @@ CalendarEvent.propTypes = {
|
|||||||
inCinemas: PropTypes.string,
|
inCinemas: PropTypes.string,
|
||||||
physicalRelease: PropTypes.string,
|
physicalRelease: PropTypes.string,
|
||||||
digitalRelease: PropTypes.string,
|
digitalRelease: PropTypes.string,
|
||||||
|
date: PropTypes.string.isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
certification: PropTypes.string,
|
certification: PropTypes.string,
|
||||||
hasFile: PropTypes.bool.isRequired,
|
hasFile: PropTypes.bool.isRequired,
|
||||||
grabbed: PropTypes.bool,
|
grabbed: PropTypes.bool,
|
||||||
queueItem: PropTypes.object,
|
queueItem: PropTypes.object,
|
||||||
showMovieInformation: PropTypes.bool.isRequired,
|
// These props come from the connector, not marked as required to appease TS for now.
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
showMovieInformation: PropTypes.bool,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
showCutoffUnmetIcon: PropTypes.bool,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired,
|
fullColorEvents: PropTypes.bool,
|
||||||
date: PropTypes.string.isRequired
|
timeFormat: PropTypes.string,
|
||||||
|
colorImpairedMode: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
CalendarEvent.defaultProps = {
|
CalendarEvent.defaultProps = {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import QueueDetails from 'Activity/Queue/QueueDetails';
|
import QueueDetails from 'Activity/Queue/QueueDetails';
|
||||||
import CircularProgressBar from 'Components/CircularProgressBar';
|
import CircularProgressBar from 'Components/CircularProgressBar';
|
||||||
import translate from 'Utilities/String/translate';
|
|
||||||
|
|
||||||
function CalendarEventQueueDetails(props) {
|
function CalendarEventQueueDetails(props) {
|
||||||
const {
|
const {
|
||||||
@@ -13,6 +12,7 @@ function CalendarEventQueueDetails(props) {
|
|||||||
status,
|
status,
|
||||||
trackedDownloadState,
|
trackedDownloadState,
|
||||||
trackedDownloadStatus,
|
trackedDownloadStatus,
|
||||||
|
statusMessages,
|
||||||
errorMessage
|
errorMessage
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -27,16 +27,15 @@ function CalendarEventQueueDetails(props) {
|
|||||||
status={status}
|
status={status}
|
||||||
trackedDownloadState={trackedDownloadState}
|
trackedDownloadState={trackedDownloadState}
|
||||||
trackedDownloadStatus={trackedDownloadStatus}
|
trackedDownloadStatus={trackedDownloadStatus}
|
||||||
|
statusMessages={statusMessages}
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
progressBar={
|
progressBar={
|
||||||
<div title={translate('MovieIsDownloadingInterp', [progress.toFixed(1), title])}>
|
<CircularProgressBar
|
||||||
<CircularProgressBar
|
progress={progress}
|
||||||
progress={progress}
|
size={20}
|
||||||
size={20}
|
strokeWidth={2}
|
||||||
strokeWidth={2}
|
strokeColor={'#7a43b6'}
|
||||||
strokeColor={'#7a43b6'}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -50,6 +49,7 @@ CalendarEventQueueDetails.propTypes = {
|
|||||||
status: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
trackedDownloadState: PropTypes.string.isRequired,
|
trackedDownloadState: PropTypes.string.isRequired,
|
||||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||||
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
errorMessage: PropTypes.string
|
errorMessage: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,18 +8,21 @@ import styles from './Legend.css';
|
|||||||
|
|
||||||
function Legend(props) {
|
function Legend(props) {
|
||||||
const {
|
const {
|
||||||
|
view,
|
||||||
showCutoffUnmetIcon,
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
colorImpairedMode
|
colorImpairedMode
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const iconsToShow = [];
|
const iconsToShow = [];
|
||||||
|
const isAgendaView = view === 'agenda';
|
||||||
|
|
||||||
if (showCutoffUnmetIcon) {
|
if (showCutoffUnmetIcon) {
|
||||||
iconsToShow.push(
|
iconsToShow.push(
|
||||||
<LegendIconItem
|
<LegendIconItem
|
||||||
name={translate('CutoffUnmet')}
|
name={translate('CutoffUnmet')}
|
||||||
icon={icons.MOVIE_FILE}
|
icon={icons.MOVIE_FILE}
|
||||||
kind={kinds.WARNING}
|
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||||
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
|
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -29,45 +32,58 @@ function Legend(props) {
|
|||||||
<div className={styles.legend}>
|
<div className={styles.legend}>
|
||||||
<div>
|
<div>
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='ended'
|
status="downloaded"
|
||||||
name={translate('DownloadedAndMonitored')}
|
name={translate('DownloadedAndMonitored')}
|
||||||
|
isAgendaView={isAgendaView}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='availNotMonitored'
|
status="unmonitored"
|
||||||
name={translate('DownloadedButNotMonitored')}
|
name={translate('DownloadedButNotMonitored')}
|
||||||
|
isAgendaView={isAgendaView}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='missingMonitored'
|
status="missingMonitored"
|
||||||
name={translate('MissingMonitoredAndConsideredAvailable')}
|
name={translate('MissingMonitoredAndConsideredAvailable')}
|
||||||
|
isAgendaView={isAgendaView}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='missingUnmonitored'
|
status="missingUnmonitored"
|
||||||
name={translate('MissingNotMonitored')}
|
name={translate('MissingNotMonitored')}
|
||||||
|
isAgendaView={isAgendaView}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='queue'
|
status="queue"
|
||||||
name={translate('Queued')}
|
name={translate('Queued')}
|
||||||
|
isAgendaView={isAgendaView}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LegendItem
|
<LegendItem
|
||||||
style='continuing'
|
status="continuing"
|
||||||
name={translate('Unreleased')}
|
name={translate('Unreleased')}
|
||||||
|
isAgendaView={isAgendaView}
|
||||||
|
fullColorEvents={fullColorEvents}
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
iconsToShow.length > 0 &&
|
iconsToShow.length > 0 &&
|
||||||
<div>
|
<div>
|
||||||
@@ -79,7 +95,9 @@ function Legend(props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Legend.propTypes = {
|
Legend.propTypes = {
|
||||||
|
view: PropTypes.string.isRequired,
|
||||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
colorImpairedMode: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import Legend from './Legend';
|
|||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.calendar.options,
|
(state) => state.calendar.options,
|
||||||
|
(state) => state.calendar.view,
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
(calendarOptions, uiSettings) => {
|
(calendarOptions, view, uiSettings) => {
|
||||||
return {
|
return {
|
||||||
...calendarOptions,
|
...calendarOptions,
|
||||||
|
view,
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ function LegendIconItem(props) {
|
|||||||
name,
|
name,
|
||||||
icon,
|
icon,
|
||||||
kind,
|
kind,
|
||||||
|
darken,
|
||||||
tooltip
|
tooltip
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ function LegendIconItem(props) {
|
|||||||
<Icon
|
<Icon
|
||||||
className={styles.icon}
|
className={styles.icon}
|
||||||
name={icon}
|
name={icon}
|
||||||
|
darken={darken}
|
||||||
kind={kind}
|
kind={kind}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -31,7 +33,12 @@ LegendIconItem.propTypes = {
|
|||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
icon: PropTypes.object.isRequired,
|
icon: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
|
darken: PropTypes.bool.isRequired,
|
||||||
tooltip: PropTypes.string.isRequired
|
tooltip: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
LegendIconItem.defaultProps = {
|
||||||
|
darken: false
|
||||||
|
};
|
||||||
|
|
||||||
export default LegendIconItem;
|
export default LegendIconItem;
|
||||||
|
|||||||
@@ -1,74 +1,37 @@
|
|||||||
.legendItemContainer {
|
|
||||||
margin-right: 5px;
|
|
||||||
width: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legendItem {
|
.legendItem {
|
||||||
display: inline-flex;
|
margin: 3px 0;
|
||||||
margin-top: -1px;
|
margin-right: 6px;
|
||||||
vertical-align: middle;
|
padding-left: 5px;
|
||||||
line-height: 16px;
|
width: 220px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
border-left-style: solid;
|
||||||
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legendItemColor {
|
/*
|
||||||
margin-right: 8px;
|
* Status
|
||||||
width: 30px;
|
*/
|
||||||
height: 16px;
|
|
||||||
border-radius: 4px;
|
.downloaded {
|
||||||
|
composes: downloaded from '~Calendar/Events/CalendarEvent.css';
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue {
|
.queue {
|
||||||
composes: legendItemColor;
|
composes: queue from '~Calendar/Events/CalendarEvent.css';
|
||||||
|
|
||||||
background-color: var(--queueColor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.continuing {
|
.unmonitored {
|
||||||
composes: legendItemColor;
|
composes: unmonitored from '~Calendar/Events/CalendarEvent.css';
|
||||||
|
|
||||||
background-color: var(--primaryColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.availNotMonitored {
|
|
||||||
composes: legendItemColor;
|
|
||||||
|
|
||||||
background-color: var(--darkGray);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ended {
|
|
||||||
composes: legendItemColor;
|
|
||||||
|
|
||||||
background-color: var(--successColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
.missingMonitored {
|
|
||||||
composes: legendItemColor;
|
|
||||||
|
|
||||||
background-color: var(--dangerColor);
|
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
|
||||||
background: repeating-linear-gradient(90deg, color(var(--dangerColor) shade(5%)), color(var(--dangerColor) shade(5%)) 5px, color(var(--dangerColor) shade(15%)) 5px, color(var(--dangerColor) shade(15%)) 10px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingUnmonitored {
|
.missingUnmonitored {
|
||||||
composes: legendItemColor;
|
composes: missingUnmonitored from '~Calendar/Events/CalendarEvent.css';
|
||||||
|
|
||||||
background-color: var(--warningColor);
|
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
|
||||||
background: repeating-linear-gradient(45deg, var(--warningColor), var(--warningColor) 5px, color(var(--warningColor) tint(15%)) 5px, color(var(--warningColor) tint(15%)) 10px);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingMonitoredColorImpaired {
|
.missingMonitored {
|
||||||
background: repeating-linear-gradient(90deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
composes: missingMonitored from '~Calendar/Events/CalendarEvent.css';
|
||||||
}
|
}
|
||||||
|
|
||||||
.missingUnmonitoredColorImpaired {
|
.continuing {
|
||||||
background: repeating-linear-gradient(45deg, var(--colorImpairedGradientDark), var(--colorImpairedGradientDark) 5px, var(--colorImpairedGradient) 5px, var(--colorImpairedGradient) 10px);
|
composes: continuing from '~Calendar/Events/CalendarEvent.css';
|
||||||
}
|
|
||||||
|
|
||||||
.legendItemText {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'availNotMonitored': string;
|
|
||||||
'continuing': string;
|
'continuing': string;
|
||||||
'ended': string;
|
'downloaded': string;
|
||||||
'legendItem': string;
|
'legendItem': string;
|
||||||
'legendItemColor': string;
|
|
||||||
'legendItemContainer': string;
|
|
||||||
'legendItemText': string;
|
|
||||||
'missingMonitored': string;
|
'missingMonitored': string;
|
||||||
'missingMonitoredColorImpaired': string;
|
|
||||||
'missingUnmonitored': string;
|
'missingUnmonitored': string;
|
||||||
'missingUnmonitoredColorImpaired': string;
|
|
||||||
'queue': string;
|
'queue': string;
|
||||||
|
'unmonitored': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -6,29 +6,31 @@ import styles from './LegendItem.css';
|
|||||||
function LegendItem(props) {
|
function LegendItem(props) {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
style,
|
status,
|
||||||
|
isAgendaView,
|
||||||
|
fullColorEvents,
|
||||||
colorImpairedMode
|
colorImpairedMode
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.legendItemContainer}>
|
<div
|
||||||
<div
|
className={classNames(
|
||||||
className={classNames(
|
styles.legendItem,
|
||||||
styles.legendItem,
|
styles[status],
|
||||||
styles[style],
|
colorImpairedMode && 'colorImpaired',
|
||||||
colorImpairedMode && 'colorImpaired'
|
fullColorEvents && !isAgendaView && 'fullColor'
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
<div className={classNames(styles.legendItemText, colorImpairedMode && styles[`${style}ColorImpaired`])}>
|
{name}
|
||||||
{name}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
LegendItem.propTypes = {
|
LegendItem.propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
style: PropTypes.string.isRequired,
|
status: PropTypes.string.isRequired,
|
||||||
|
isAgendaView: PropTypes.bool.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
colorImpairedMode: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,14 +26,16 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
firstDayOfWeek,
|
firstDayOfWeek,
|
||||||
calendarWeekColumnHeader,
|
calendarWeekColumnHeader,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode,
|
||||||
|
fullColorEvents
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
firstDayOfWeek,
|
firstDayOfWeek,
|
||||||
calendarWeekColumnHeader,
|
calendarWeekColumnHeader,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode,
|
||||||
|
fullColorEvents
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
const {
|
const {
|
||||||
showMovieInformation,
|
showMovieInformation,
|
||||||
showCutoffUnmetIcon,
|
showCutoffUnmetIcon,
|
||||||
|
fullColorEvents,
|
||||||
onModalClose
|
onModalClose
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -136,6 +139,18 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
onChange={this.onOptionInputChange}
|
onChange={this.onOptionInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('FullColorEvents')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.CHECK}
|
||||||
|
name="fullColorEvents"
|
||||||
|
value={fullColorEvents}
|
||||||
|
helpText={translate('FullColorEventsHelpText')}
|
||||||
|
onChange={this.onOptionInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</Form>
|
</Form>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
@@ -176,7 +191,9 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
value={timeFormat}
|
value={timeFormat}
|
||||||
onChange={this.onGlobalInputChange}
|
onChange={this.onGlobalInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup><FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
|
<FormLabel>{translate('EnableColorImpairedMode')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
@@ -187,7 +204,6 @@ class CalendarOptionsModalContent extends Component {
|
|||||||
onChange={this.onGlobalInputChange}
|
onChange={this.onGlobalInputChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
</Form>
|
</Form>
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
@@ -209,6 +225,7 @@ CalendarOptionsModalContent.propTypes = {
|
|||||||
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
calendarWeekColumnHeader: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
fullColorEvents: PropTypes.bool.isRequired,
|
||||||
dispatchSetCalendarOption: PropTypes.func.isRequired,
|
dispatchSetCalendarOption: PropTypes.func.isRequired,
|
||||||
dispatchSaveUISettings: PropTypes.func.isRequired,
|
dispatchSaveUISettings: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ function createMapStateToProps() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...collection,
|
...collection,
|
||||||
|
movies: [...collection.movies].sort((a, b) => b.year - a.year),
|
||||||
genres: Array.from(new Set(allGenres)).slice(0, 3)
|
genres: Array.from(new Set(allGenres)).slice(0, 3)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class CollectionMovie extends Component {
|
|||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
status,
|
||||||
overview,
|
overview,
|
||||||
year,
|
year,
|
||||||
tmdbId,
|
tmdbId,
|
||||||
@@ -123,11 +124,11 @@ class CollectionMovie extends Component {
|
|||||||
|
|
||||||
<div className={styles.overlay}>
|
<div className={styles.overlay}>
|
||||||
<div className={styles.overlayTitle}>
|
<div className={styles.overlayTitle}>
|
||||||
{title}
|
{title} {year > 0 ? `(${year})` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{
|
{
|
||||||
id &&
|
id ?
|
||||||
<div className={styles.overlayStatus}>
|
<div className={styles.overlayStatus}>
|
||||||
<MovieIndexProgressBar
|
<MovieIndexProgressBar
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
@@ -138,7 +139,8 @@ class CollectionMovie extends Component {
|
|||||||
detailedProgressBar={detailedProgressBar}
|
detailedProgressBar={detailedProgressBar}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -171,6 +173,7 @@ CollectionMovie.propTypes = {
|
|||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
year: PropTypes.number.isRequired,
|
year: PropTypes.number.isRequired,
|
||||||
|
status: PropTypes.string.isRequired,
|
||||||
overview: PropTypes.string.isRequired,
|
overview: PropTypes.string.isRequired,
|
||||||
monitored: PropTypes.bool,
|
monitored: PropTypes.bool,
|
||||||
collectionId: PropTypes.number.isRequired,
|
collectionId: PropTypes.number.isRequired,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
margin: 2px 4px;
|
margin: 2px 4px;
|
||||||
border: 1px solid var(--borderColor);
|
border: 1px solid var(--borderColor);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: #eee;
|
background-color: var(--inputBackgroundColor);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
border-left: 4px;
|
border-left: 4px;
|
||||||
border-left-style: solid;
|
border-left-style: solid;
|
||||||
background-color: var(--white);
|
background-color: var(--themeLightColor);
|
||||||
color: var(--defaultColor);
|
color: var(--defaultColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ class CollectionMovieLabel extends Component {
|
|||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
|
year,
|
||||||
status,
|
status,
|
||||||
monitored,
|
monitored,
|
||||||
isAvailable,
|
isAvailable,
|
||||||
@@ -35,9 +36,7 @@ class CollectionMovieLabel extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
<span>
|
<span>
|
||||||
{
|
{title} {year > 0 ? `(${year})` : ''}
|
||||||
title
|
|
||||||
}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -62,6 +61,7 @@ class CollectionMovieLabel extends Component {
|
|||||||
CollectionMovieLabel.propTypes = {
|
CollectionMovieLabel.propTypes = {
|
||||||
id: PropTypes.number,
|
id: PropTypes.number,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
|
year: PropTypes.number.isRequired,
|
||||||
status: PropTypes.string,
|
status: PropTypes.string,
|
||||||
isAvailable: PropTypes.bool,
|
isAvailable: PropTypes.bool,
|
||||||
monitored: PropTypes.bool,
|
monitored: PropTypes.bool,
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ function calculatePosterWidth(posterSize, isSmallScreen) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
function calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions) {
|
||||||
|
|
||||||
const heights = [
|
const heights = [
|
||||||
overviewOptions.showPosters ? posterHeight : 75,
|
overviewOptions.showPosters ? posterHeight : 75,
|
||||||
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
isSmallScreen ? columnPaddingSmallScreen : columnPadding
|
||||||
@@ -122,8 +121,8 @@ class CollectionOverviews extends Component {
|
|||||||
overviewOptions
|
overviewOptions
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const posterWidth = calculatePosterWidth(overviewOptions.size, isSmallScreen);
|
const posterWidth = overviewOptions.showPosters ? calculatePosterWidth(overviewOptions.size, isSmallScreen) : 0;
|
||||||
const posterHeight = calculatePosterHeight(posterWidth);
|
const posterHeight = overviewOptions.showPosters ? calculatePosterHeight(posterWidth) : 0;
|
||||||
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
const rowHeight = calculateRowHeight(posterHeight, sortKey, isSmallScreen, overviewOptions);
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
|
|||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
|
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
|
||||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
|
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||||
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
|
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
|
||||||
|
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
|
||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
|
||||||
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector';
|
||||||
@@ -58,9 +61,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.DATE:
|
case filterBuilderValueTypes.DATE:
|
||||||
return DateFilterBuilderRowValue;
|
return DateFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||||
|
return HistoryEventTypeFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.INDEXER:
|
case filterBuilderValueTypes.INDEXER:
|
||||||
return IndexerFilterBuilderRowValueConnector;
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.LANGUAGE:
|
||||||
|
return LanguageFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.PROTOCOL:
|
case filterBuilderValueTypes.PROTOCOL:
|
||||||
return ProtocolFilterBuilderRowValue;
|
return ProtocolFilterBuilderRowValue;
|
||||||
|
|
||||||
@@ -70,6 +79,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.QUALITY_PROFILE:
|
case filterBuilderValueTypes.QUALITY_PROFILE:
|
||||||
return QualityProfileFilterBuilderRowValueConnector;
|
return QualityProfileFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.MOVIE:
|
||||||
|
return MovieFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.RELEASE_STATUS:
|
case filterBuilderValueTypes.RELEASE_STATUS:
|
||||||
return ReleaseStatusFilterBuilderRowValue;
|
return ReleaseStatusFilterBuilderRowValue;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { FilterBuilderProp } from 'App/State/AppState';
|
||||||
|
|
||||||
|
interface FilterBuilderRowOnChangeProps {
|
||||||
|
name: string;
|
||||||
|
value: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterBuilderRowValueProps {
|
||||||
|
filterType?: string;
|
||||||
|
filterValue: string | number | object | string[] | number[] | object[];
|
||||||
|
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||||
|
sectionItem: unknown[];
|
||||||
|
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterBuilderRowValueProps;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
.tag {
|
.tag {
|
||||||
height: 21px;
|
display: flex;
|
||||||
|
|
||||||
&.isLastTag {
|
&.isLastTag {
|
||||||
.or {
|
.or {
|
||||||
@@ -18,4 +18,5 @@
|
|||||||
.or {
|
.or {
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
color: var(--themeDarkColor);
|
color: var(--themeDarkColor);
|
||||||
|
line-height: 31px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import PropTypes from 'prop-types';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TagInputTag from 'Components/Form/TagInputTag';
|
import TagInputTag from 'Components/Form/TagInputTag';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './FilterBuilderRowValueTag.css';
|
import styles from './FilterBuilderRowValueTag.css';
|
||||||
|
|
||||||
function FilterBuilderRowValueTag(props) {
|
function FilterBuilderRowValueTag(props) {
|
||||||
return (
|
return (
|
||||||
<span
|
<div
|
||||||
className={styles.tag}
|
className={styles.tag}
|
||||||
>
|
>
|
||||||
<TagInputTag
|
<TagInputTag
|
||||||
@@ -15,12 +16,13 @@ function FilterBuilderRowValueTag(props) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{
|
{
|
||||||
!props.isLastTag &&
|
props.isLastTag ?
|
||||||
<span className={styles.or}>
|
null :
|
||||||
or
|
<div className={styles.or}>
|
||||||
</span>
|
{translate('Or')}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
const EVENT_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
get name() {
|
||||||
|
return translate('Grabbed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
get name() {
|
||||||
|
return translate('Imported');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
get name() {
|
||||||
|
return translate('Failed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
get name() {
|
||||||
|
return translate('Deleted');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 8,
|
||||||
|
get name() {
|
||||||
|
return translate('Renamed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9,
|
||||||
|
get name() {
|
||||||
|
return translate('Ignored');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function HistoryEventTypeFilterBuilderRowValue(
|
||||||
|
props: FilterBuilderRowValueProps
|
||||||
|
) {
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryEventTypeFilterBuilderRowValue;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
const { items } = useSelector(createLanguagesSelector());
|
||||||
|
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={items} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LanguageFilterBuilderRowValue;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import Movie from 'Movie/Movie';
|
||||||
|
import createAllMoviesSelector from 'Store/Selectors/createAllMoviesSelector';
|
||||||
|
import sortByName from 'Utilities/Array/sortByName';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
function MovieFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||||
|
const allMovies: Movie[] = useSelector(createAllMoviesSelector());
|
||||||
|
|
||||||
|
const tagList = allMovies
|
||||||
|
.map((movie) => ({ id: movie.id, name: movie.title }))
|
||||||
|
.sort(sortByName);
|
||||||
|
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieFilterBuilderRowValue;
|
||||||
@@ -12,7 +12,7 @@ import DownloadClientSelectInputConnector from './DownloadClientSelectInputConne
|
|||||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
import EnhancedSelectInputConnector from './EnhancedSelectInputConnector';
|
||||||
import FormInputHelpText from './FormInputHelpText';
|
import FormInputHelpText from './FormInputHelpText';
|
||||||
import IndexerFlagsSelectInputConnector from './IndexerFlagsSelectInputConnector';
|
import IndexerFlagsSelectInput from './IndexerFlagsSelectInput';
|
||||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||||
import KeyValueListInput from './KeyValueListInput';
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
import LanguageSelectInputConnector from './LanguageSelectInputConnector';
|
||||||
@@ -76,7 +76,7 @@ function getComponent(type) {
|
|||||||
return RootFolderSelectInputConnector;
|
return RootFolderSelectInputConnector;
|
||||||
|
|
||||||
case inputTypes.INDEXER_FLAGS_SELECT:
|
case inputTypes.INDEXER_FLAGS_SELECT:
|
||||||
return IndexerFlagsSelectInputConnector;
|
return IndexerFlagsSelectInput;
|
||||||
|
|
||||||
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
case inputTypes.DOWNLOAD_CLIENT_SELECT:
|
||||||
return DownloadClientSelectInputConnector;
|
return DownloadClientSelectInputConnector;
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
margin-right: $formLabelRightMarginWidth;
|
margin-right: $formLabelRightMarginWidth;
|
||||||
|
padding-top: 8px;
|
||||||
|
min-height: 35px;
|
||||||
|
text-align: end;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 35px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasError {
|
.hasError {
|
||||||
|
|||||||
60
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal file
60
frontend/src/Components/Form/IndexerFlagsSelectInput.tsx
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||||
|
|
||||||
|
interface IndexerFlagsSelectInputProps {
|
||||||
|
name: string;
|
||||||
|
indexerFlags: number;
|
||||||
|
onChange(payload: object): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectIndexerFlagsValues = (selectedFlags: number) =>
|
||||||
|
createSelector(
|
||||||
|
(state: AppState) => state.settings.indexerFlags,
|
||||||
|
(indexerFlags) => {
|
||||||
|
const value = indexerFlags.items
|
||||||
|
.filter(
|
||||||
|
// eslint-disable-next-line no-bitwise
|
||||||
|
(item) => (selectedFlags & item.id) === item.id
|
||||||
|
)
|
||||||
|
.map(({ id }) => id);
|
||||||
|
|
||||||
|
const values = indexerFlags.items.map(({ id, name }) => ({
|
||||||
|
key: id,
|
||||||
|
value: name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function IndexerFlagsSelectInput(props: IndexerFlagsSelectInputProps) {
|
||||||
|
const { indexerFlags, onChange } = props;
|
||||||
|
|
||||||
|
const { value, values } = useSelector(selectIndexerFlagsValues(indexerFlags));
|
||||||
|
|
||||||
|
const onChangeWrapper = useCallback(
|
||||||
|
({ name, value }: { name: string; value: number[] }) => {
|
||||||
|
const indexerFlags = value.reduce((acc, flagId) => acc + flagId, 0);
|
||||||
|
|
||||||
|
onChange({ name, value: indexerFlags });
|
||||||
|
},
|
||||||
|
[onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnhancedSelectInput
|
||||||
|
{...props}
|
||||||
|
value={value}
|
||||||
|
values={values}
|
||||||
|
onChange={onChangeWrapper}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IndexerFlagsSelectInput;
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
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);
|
|
||||||
@@ -37,6 +37,8 @@ function getType({ type, selectOptionsProviderAction }) {
|
|||||||
return inputTypes.OAUTH;
|
return inputTypes.OAUTH;
|
||||||
case 'rootFolder':
|
case 'rootFolder':
|
||||||
return inputTypes.ROOT_FOLDER_SELECT;
|
return inputTypes.ROOT_FOLDER_SELECT;
|
||||||
|
case 'qualityProfile':
|
||||||
|
return inputTypes.QUALITY_PROFILE_SELECT;
|
||||||
default:
|
default:
|
||||||
return inputTypes.TEXT;
|
return inputTypes.TEXT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||||
|
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import RootFolderSelectInput from './RootFolderSelectInput';
|
import RootFolderSelectInput from './RootFolderSelectInput';
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ const ADD_NEW_KEY = 'addNew';
|
|||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.rootFolders,
|
createRootFoldersSelector(),
|
||||||
(state, { value }) => value,
|
(state, { value }) => value,
|
||||||
(state, { includeMissingValue }) => includeMissingValue,
|
(state, { includeMissingValue }) => includeMissingValue,
|
||||||
(state, { includeNoChange }) => includeNoChange,
|
(state, { includeNoChange }) => includeNoChange,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class UMaskInput extends Component {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div>
|
<div>
|
||||||
<label>{translate('UMask')}</label>
|
<label>{translate('Umask')}</label>
|
||||||
<div className={styles.value}>{umask}</div>
|
<div className={styles.value}>{umask}</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,10 +12,18 @@
|
|||||||
|
|
||||||
.info {
|
.info {
|
||||||
color: var(--infoColor);
|
color: var(--infoColor);
|
||||||
|
|
||||||
|
&:global(.darken) {
|
||||||
|
color: color(var(--infoColor) shade(30%));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.pink {
|
.pink {
|
||||||
color: var(--pink);
|
color: var(--pink);
|
||||||
|
|
||||||
|
&:global(.darken) {
|
||||||
|
color: color(var(--pink) shade(30%));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ class Icon extends PureComponent {
|
|||||||
kind,
|
kind,
|
||||||
size,
|
size,
|
||||||
title,
|
title,
|
||||||
|
darken,
|
||||||
isSpinning,
|
isSpinning,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -26,7 +27,8 @@ class Icon extends PureComponent {
|
|||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
styles[kind]
|
styles[kind],
|
||||||
|
darken && 'darken'
|
||||||
)}
|
)}
|
||||||
icon={name}
|
icon={name}
|
||||||
spin={isSpinning}
|
spin={isSpinning}
|
||||||
@@ -59,6 +61,7 @@ Icon.propTypes = {
|
|||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
size: PropTypes.number.isRequired,
|
size: PropTypes.number.isRequired,
|
||||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||||
|
darken: PropTypes.bool.isRequired,
|
||||||
isSpinning: PropTypes.bool.isRequired,
|
isSpinning: PropTypes.bool.isRequired,
|
||||||
fixedWidth: PropTypes.bool.isRequired
|
fixedWidth: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
@@ -66,6 +69,7 @@ Icon.propTypes = {
|
|||||||
Icon.defaultProps = {
|
Icon.defaultProps = {
|
||||||
kind: kinds.DEFAULT,
|
kind: kinds.DEFAULT,
|
||||||
size: 14,
|
size: 14,
|
||||||
|
darken: false,
|
||||||
isSpinning: false,
|
isSpinning: false,
|
||||||
fixedWidth: false
|
fixedWidth: false
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ class InlineMarkdown extends Component {
|
|||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
data
|
data,
|
||||||
|
blockClassName
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
// For now only replace links or code blocks (not both)
|
// For now only replace links or code blocks (not both)
|
||||||
@@ -47,7 +48,7 @@ class InlineMarkdown extends Component {
|
|||||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
markdownBlocks.push(<code key={`code-${match.index}`}>{match[0].substring(1, match[0].length - 1)}</code>);
|
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>);
|
||||||
endIndex = match.index + match[0].length;
|
endIndex = match.index + match[0].length;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +67,8 @@ class InlineMarkdown extends Component {
|
|||||||
|
|
||||||
InlineMarkdown.propTypes = {
|
InlineMarkdown.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
data: PropTypes.string
|
data: PropTypes.string,
|
||||||
|
blockClassName: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InlineMarkdown;
|
export default InlineMarkdown;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import React from 'react';
|
|||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
import styles from './ModalContent.css';
|
import styles from './ModalContent.css';
|
||||||
|
|
||||||
function ModalContent(props) {
|
function ModalContent(props) {
|
||||||
@@ -28,6 +29,7 @@ function ModalContent(props) {
|
|||||||
<Icon
|
<Icon
|
||||||
name={icons.CLOSE}
|
name={icons.CLOSE}
|
||||||
size={18}
|
size={18}
|
||||||
|
title={translate('Close')}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class PageHeader extends Component {
|
|||||||
aria-label="Donate"
|
aria-label="Donate"
|
||||||
to="https://radarr.video/donate"
|
to="https://radarr.video/donate"
|
||||||
size={14}
|
size={14}
|
||||||
|
title={translate('Donate')}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.translate}
|
className={styles.translate}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
|
|||||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.INTERACTIVE}
|
name={icons.INTERACTIVE}
|
||||||
|
title={translate('Menu')}
|
||||||
/>
|
/>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ class TableOptionsModal extends Component {
|
|||||||
let pageSizeError = null;
|
let pageSizeError = null;
|
||||||
|
|
||||||
if (value < 5) {
|
if (value < 5) {
|
||||||
pageSizeError = 'Page size must be at least 5';
|
pageSizeError = translate('TablePageSizeMinimum', { minimumValue: '5' });
|
||||||
} else if (value > 250) {
|
} else if (value > 250) {
|
||||||
pageSizeError = 'Page size must not exceed 250';
|
pageSizeError = translate('TablePageSizeMaximum', { maximumValue: '250' });
|
||||||
} else {
|
} else {
|
||||||
this.props.onTableOptionChange({ pageSize: value });
|
this.props.onTableOptionChange({ pageSize: value });
|
||||||
}
|
}
|
||||||
@@ -145,13 +145,13 @@ class TableOptionsModal extends Component {
|
|||||||
{
|
{
|
||||||
hasPageSize ?
|
hasPageSize ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('PageSize')}</FormLabel>
|
<FormLabel>{translate('TablePageSize')}</FormLabel>
|
||||||
|
|
||||||
<FormInputGroup
|
<FormInputGroup
|
||||||
type={inputTypes.NUMBER}
|
type={inputTypes.NUMBER}
|
||||||
name="pageSize"
|
name="pageSize"
|
||||||
value={pageSize || 0}
|
value={pageSize || 0}
|
||||||
helpText={translate('PageSizeHelpText')}
|
helpText={translate('TablePageSizeHelpText')}
|
||||||
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
|
errors={pageSizeError ? [{ message: pageSizeError }] : undefined}
|
||||||
onChange={this.onPageSizeChange}
|
onChange={this.onPageSizeChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ function createMapDispatchToProps(dispatch, props) {
|
|||||||
|
|
||||||
onImportListSyncPress() {
|
onImportListSyncPress() {
|
||||||
dispatch(executeCommand({
|
dispatch(executeCommand({
|
||||||
name: commandNames.IMPORT_LIST_SYNC
|
name: commandNames.IMPORT_LIST_SYNC,
|
||||||
|
commandFinished: this.dispatchFetchListMovies
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
|
|||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
authenticationRequired,
|
authenticationRequired,
|
||||||
username,
|
username,
|
||||||
password
|
password,
|
||||||
|
passwordConfirmation
|
||||||
} = settings;
|
} = settings;
|
||||||
|
|
||||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||||
@@ -63,7 +64,7 @@ function AuthenticationRequiredModalContent(props) {
|
|||||||
className={styles.authRequiredAlert}
|
className={styles.authRequiredAlert}
|
||||||
kind={kinds.WARNING}
|
kind={kinds.WARNING}
|
||||||
>
|
>
|
||||||
{translate('AuthenticationRequiredWarning', { appName: 'Radarr' })}
|
{translate('AuthenticationRequiredWarning')}
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -76,7 +77,7 @@ function AuthenticationRequiredModalContent(props) {
|
|||||||
type={inputTypes.SELECT}
|
type={inputTypes.SELECT}
|
||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText', { appName: 'Radarr' })}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
|
||||||
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
|
helpLink="https://wiki.servarr.com/radarr/faq#forced-authentication"
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
@@ -120,6 +121,18 @@ function AuthenticationRequiredModalContent(props) {
|
|||||||
{...password}
|
{...password}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup>
|
||||||
|
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.PASSWORD}
|
||||||
|
name="passwordConfirmation"
|
||||||
|
onChange={onInputChange}
|
||||||
|
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||||
|
{...passwordConfirmation}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
</div> :
|
</div> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ export const BOOL = 'bool';
|
|||||||
export const BYTES = 'bytes';
|
export const BYTES = 'bytes';
|
||||||
export const DATE = 'date';
|
export const DATE = 'date';
|
||||||
export const DEFAULT = 'default';
|
export const DEFAULT = 'default';
|
||||||
|
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||||
export const INDEXER = 'indexer';
|
export const INDEXER = 'indexer';
|
||||||
|
export const LANGUAGE = 'language';
|
||||||
export const PROTOCOL = 'protocol';
|
export const PROTOCOL = 'protocol';
|
||||||
export const QUALITY = 'quality';
|
export const QUALITY = 'quality';
|
||||||
export const QUALITY_PROFILE = 'qualityProfile';
|
export const QUALITY_PROFILE = 'qualityProfile';
|
||||||
|
export const MOVIE = 'movie';
|
||||||
export const RELEASE_STATUS = 'releaseStatus';
|
export const RELEASE_STATUS = 'releaseStatus';
|
||||||
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
|
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
|
||||||
export const TAG = 'tag';
|
export const TAG = 'tag';
|
||||||
|
|||||||
@@ -52,11 +52,7 @@
|
|||||||
width: 75px;
|
width: 75px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.titleContent {
|
||||||
composes: cell;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title div {
|
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ interface CssExports {
|
|||||||
'quality': string;
|
'quality': string;
|
||||||
'rejected': string;
|
'rejected': string;
|
||||||
'size': string;
|
'size': string;
|
||||||
'title': string;
|
'titleContent': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
|||||||
@@ -246,10 +246,12 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
|||||||
) : null}
|
) : null}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.title}>
|
<TableRowCell>
|
||||||
<Link to={infoUrl} title={title}>
|
<div className={styles.titleContent}>
|
||||||
<div>{title}</div>
|
<Link to={infoUrl} title={title}>
|
||||||
</Link>
|
{title}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
|
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
|
||||||
|
|||||||
@@ -202,6 +202,12 @@
|
|||||||
.headerContent {
|
.headerContent {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: 300;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointLarge) {
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import TextTruncate from 'react-text-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
|
import Alert from 'Components/Alert';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import ImdbRating from 'Components/ImdbRating';
|
import ImdbRating from 'Components/ImdbRating';
|
||||||
import InfoLabel from 'Components/InfoLabel';
|
import InfoLabel from 'Components/InfoLabel';
|
||||||
@@ -43,7 +44,7 @@ import MovieCollectionLabelConnector from './../MovieCollectionLabelConnector';
|
|||||||
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
import MovieCastPostersConnector from './Credits/Cast/MovieCastPostersConnector';
|
||||||
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
import MovieCrewPostersConnector from './Credits/Crew/MovieCrewPostersConnector';
|
||||||
import MovieDetailsLinks from './MovieDetailsLinks';
|
import MovieDetailsLinks from './MovieDetailsLinks';
|
||||||
import MovieReleaseDatesConnector from './MovieReleaseDatesConnector';
|
import MovieReleaseDates from './MovieReleaseDates';
|
||||||
import MovieStatusLabel from './MovieStatusLabel';
|
import MovieStatusLabel from './MovieStatusLabel';
|
||||||
import MovieTagsConnector from './MovieTagsConnector';
|
import MovieTagsConnector from './MovieTagsConnector';
|
||||||
import MovieTitlesTable from './Titles/MovieTitlesTable';
|
import MovieTitlesTable from './Titles/MovieTitlesTable';
|
||||||
@@ -53,11 +54,7 @@ const defaultFontSize = parseInt(fonts.defaultFontSize);
|
|||||||
const lineHeight = parseFloat(fonts.lineHeight);
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
|
|
||||||
function getFanartUrl(images) {
|
function getFanartUrl(images) {
|
||||||
const fanartImage = _.find(images, { coverType: 'fanart' });
|
return _.find(images, { coverType: 'fanart' })?.url;
|
||||||
if (fanartImage) {
|
|
||||||
// Remove protocol
|
|
||||||
return fanartImage.url.replace(/^https?:/, '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExpandedState(newState) {
|
function getExpandedState(newState) {
|
||||||
@@ -289,7 +286,7 @@ class MovieDetails extends Component {
|
|||||||
onMonitorTogglePress,
|
onMonitorTogglePress,
|
||||||
onRefreshPress,
|
onRefreshPress,
|
||||||
onSearchPress,
|
onSearchPress,
|
||||||
queueItems,
|
queueItem,
|
||||||
movieRuntimeFormat
|
movieRuntimeFormat
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
@@ -436,7 +433,7 @@ class MovieDetails extends Component {
|
|||||||
}
|
}
|
||||||
title={translate('ReleaseDates')}
|
title={translate('ReleaseDates')}
|
||||||
body={
|
body={
|
||||||
<MovieReleaseDatesConnector
|
<MovieReleaseDates
|
||||||
inCinemas={inCinemas}
|
inCinemas={inCinemas}
|
||||||
physicalRelease={physicalRelease}
|
physicalRelease={physicalRelease}
|
||||||
digitalRelease={digitalRelease}
|
digitalRelease={digitalRelease}
|
||||||
@@ -547,7 +544,7 @@ class MovieDetails extends Component {
|
|||||||
hasMovieFiles={hasMovieFiles}
|
hasMovieFiles={hasMovieFiles}
|
||||||
monitored={monitored}
|
monitored={monitored}
|
||||||
isAvailable={isAvailable}
|
isAvailable={isAvailable}
|
||||||
queueItem={(queueItems.length > 0) ? queueItems[0] : null}
|
queueItem={queueItem}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</InfoLabel>
|
</InfoLabel>
|
||||||
@@ -634,24 +631,27 @@ class MovieDetails extends Component {
|
|||||||
|
|
||||||
<div className={styles.contentContainer}>
|
<div className={styles.contentContainer}>
|
||||||
{
|
{
|
||||||
!isFetching && movieFilesError &&
|
!isFetching && movieFilesError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingMovieFilesFailed')}
|
{translate('LoadingMovieFilesFailed')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && movieCreditsError &&
|
!isFetching && movieCreditsError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingMovieCreditsFailed')}
|
{translate('LoadingMovieCreditsFailed')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && extraFilesError &&
|
!isFetching && extraFilesError ?
|
||||||
<div>
|
<Alert kind={kinds.DANGER}>
|
||||||
{translate('LoadingMovieExtraFilesFailed')}
|
{translate('LoadingMovieExtraFilesFailed')}
|
||||||
</div>
|
</Alert> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
|
<Tabs selectedIndex={selectedTabIndex} onSelect={this.onTabSelect}>
|
||||||
@@ -830,7 +830,7 @@ MovieDetails.propTypes = {
|
|||||||
onRefreshPress: PropTypes.func.isRequired,
|
onRefreshPress: PropTypes.func.isRequired,
|
||||||
onSearchPress: PropTypes.func.isRequired,
|
onSearchPress: PropTypes.func.isRequired,
|
||||||
onGoToMovie: PropTypes.func.isRequired,
|
onGoToMovie: PropTypes.func.isRequired,
|
||||||
queueItems: PropTypes.arrayOf(PropTypes.object),
|
queueItem: PropTypes.object,
|
||||||
movieRuntimeFormat: PropTypes.string.isRequired
|
movieRuntimeFormat: PropTypes.string.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -145,6 +145,8 @@ function createMapStateToProps() {
|
|||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const queueItem = queueItems.find((item) => item.movieId === movie.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...movie,
|
...movie,
|
||||||
alternateTitles,
|
alternateTitles,
|
||||||
@@ -165,7 +167,7 @@ function createMapStateToProps() {
|
|||||||
nextMovie,
|
nextMovie,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
queueItems,
|
queueItem,
|
||||||
movieRuntimeFormat
|
movieRuntimeFormat
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
|
||||||
import styles from './MovieReleaseDates.css';
|
|
||||||
|
|
||||||
function MovieReleaseDates(props) {
|
|
||||||
const {
|
|
||||||
showRelativeDates,
|
|
||||||
shortDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
inCinemas,
|
|
||||||
physicalRelease,
|
|
||||||
digitalRelease
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
!!inCinemas &&
|
|
||||||
<div >
|
|
||||||
<div className={styles.dateIcon}>
|
|
||||||
<Icon
|
|
||||||
name={icons.IN_CINEMAS}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!!digitalRelease &&
|
|
||||||
<div >
|
|
||||||
<div className={styles.dateIcon}>
|
|
||||||
<Icon
|
|
||||||
name={icons.MOVIE_FILE}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
!!physicalRelease &&
|
|
||||||
<div >
|
|
||||||
<div className={styles.dateIcon}>
|
|
||||||
<Icon
|
|
||||||
name={icons.DISC}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{getRelativeDate(physicalRelease, shortDateFormat, showRelativeDates, { timeFormat, timeForToday: false })}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
MovieReleaseDates.propTypes = {
|
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
inCinemas: PropTypes.string,
|
|
||||||
physicalRelease: PropTypes.string,
|
|
||||||
digitalRelease: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieReleaseDates;
|
|
||||||
64
frontend/src/Movie/Details/MovieReleaseDates.tsx
Normal file
64
frontend/src/Movie/Details/MovieReleaseDates.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import styles from './MovieReleaseDates.css';
|
||||||
|
|
||||||
|
interface MovieReleaseDatesProps {
|
||||||
|
inCinemas: string;
|
||||||
|
physicalRelease: string;
|
||||||
|
digitalRelease: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MovieReleaseDates(props: MovieReleaseDatesProps) {
|
||||||
|
const { inCinemas, physicalRelease, digitalRelease } = props;
|
||||||
|
|
||||||
|
const { showRelativeDates, shortDateFormat, timeFormat } = useSelector(
|
||||||
|
createUISettingsSelector()
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{inCinemas ? (
|
||||||
|
<div title={translate('InCinemas')}>
|
||||||
|
<div className={styles.dateIcon}>
|
||||||
|
<Icon name={icons.IN_CINEMAS} />
|
||||||
|
</div>
|
||||||
|
{getRelativeDate(inCinemas, shortDateFormat, showRelativeDates, {
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: false,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{digitalRelease ? (
|
||||||
|
<div title={translate('DigitalRelease')}>
|
||||||
|
<div className={styles.dateIcon}>
|
||||||
|
<Icon name={icons.MOVIE_FILE} />
|
||||||
|
</div>
|
||||||
|
{getRelativeDate(digitalRelease, shortDateFormat, showRelativeDates, {
|
||||||
|
timeFormat,
|
||||||
|
timeForToday: false,
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{physicalRelease ? (
|
||||||
|
<div title={translate('PhysicalRelease')}>
|
||||||
|
<div className={styles.dateIcon}>
|
||||||
|
<Icon name={icons.DISC} />
|
||||||
|
</div>
|
||||||
|
{getRelativeDate(
|
||||||
|
physicalRelease,
|
||||||
|
shortDateFormat,
|
||||||
|
showRelativeDates,
|
||||||
|
{ timeFormat, timeForToday: false }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MovieReleaseDates;
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
|
||||||
import MovieReleaseDates from './MovieReleaseDates';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
createUISettingsSelector(),
|
|
||||||
(uiSettings) => {
|
|
||||||
return {
|
|
||||||
showRelativeDates: uiSettings.showRelativeDates,
|
|
||||||
shortDateFormat: uiSettings.shortDateFormat,
|
|
||||||
longDateFormat: uiSettings.longDateFormat,
|
|
||||||
timeFormat: uiSettings.timeFormat
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, null)(MovieReleaseDates);
|
|
||||||
@@ -8,7 +8,6 @@ import translate from 'Utilities/String/translate';
|
|||||||
import styles from './MovieStatusLabel.css';
|
import styles from './MovieStatusLabel.css';
|
||||||
|
|
||||||
function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
|
function getMovieStatus(hasFile, isMonitored, isAvailable, queueItem = false) {
|
||||||
|
|
||||||
if (queueItem) {
|
if (queueItem) {
|
||||||
const queueStatus = queueItem.status;
|
const queueStatus = queueItem.status;
|
||||||
const queueState = queueItem.trackedDownloadStatus;
|
const queueState = queueItem.trackedDownloadStatus;
|
||||||
@@ -116,8 +115,4 @@ MovieStatusLabel.propTypes = {
|
|||||||
colorImpairedMode: PropTypes.bool
|
colorImpairedMode: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
MovieStatusLabel.defaultProps = {
|
|
||||||
title: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MovieStatusLabel;
|
export default MovieStatusLabel;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user