mirror of
https://github.com/Radarr/Radarr.git
synced 2026-03-23 17:14:46 -04:00
Compare commits
152 Commits
v5.2.4.832
...
v5.3.3.853
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4eb4128a89 | ||
|
|
f90cdbb112 | ||
|
|
a8dbc97921 | ||
|
|
f93e136386 | ||
|
|
a70fa0fcfe | ||
|
|
c8931784a7 | ||
|
|
f601448a65 | ||
|
|
64125a31b6 | ||
|
|
2f4da90d8a | ||
|
|
20d9db2cde | ||
|
|
5b7c0a94fb | ||
|
|
1416f7898e | ||
|
|
f9cd9f3204 | ||
|
|
99ab65f790 | ||
|
|
82fb355930 | ||
|
|
83d437cbb3 | ||
|
|
4beb5b328b | ||
|
|
23830f50ac | ||
|
|
b808a92cdf | ||
|
|
3185c73659 | ||
|
|
7dc9ec03a5 | ||
|
|
33228335e3 | ||
|
|
833340f8bd | ||
|
|
0ecb1d0706 | ||
|
|
25b77eb4a2 | ||
|
|
b946173c05 | ||
|
|
e5ccc32a37 | ||
|
|
3aeb52c3fd | ||
|
|
c717989034 | ||
|
|
806b89abbe | ||
|
|
cc7104a814 | ||
|
|
84c2d7f69d | ||
|
|
fcd187970c | ||
|
|
34eb59dde4 | ||
|
|
31b66c6673 | ||
|
|
06a96ef2d1 | ||
|
|
c77ce2459c | ||
|
|
083989d151 | ||
|
|
c003fe16de | ||
|
|
bc9b2cd283 | ||
|
|
d0e400c55a | ||
|
|
77863dc2cf | ||
|
|
18dc6f60b0 | ||
|
|
49501a55ae | ||
|
|
d5d77a4f1a | ||
|
|
0ae8952b38 | ||
|
|
6292ff76b0 | ||
|
|
646d271e81 | ||
|
|
3d2ca830bc | ||
|
|
da02ec3b04 | ||
|
|
cc9a443473 | ||
|
|
81b6bf521d | ||
|
|
7edb892eb4 | ||
|
|
3b36921787 | ||
|
|
c2d8bc85d0 | ||
|
|
3e55b1cf25 | ||
|
|
0b0c93081d | ||
|
|
91fbad72c0 | ||
|
|
35651ac59b | ||
|
|
1932aec131 | ||
|
|
ea470b4ee9 | ||
|
|
1bb404a912 | ||
|
|
374d20634d | ||
|
|
60d9aacac6 | ||
|
|
c5992ed944 | ||
|
|
4c4073ce1c | ||
|
|
d72f78d979 | ||
|
|
dca9d69aaa | ||
|
|
5a64826868 | ||
|
|
cda40312e0 | ||
|
|
907779b4ce | ||
|
|
cc03651af5 | ||
|
|
1ae98d618c | ||
|
|
f5914da2f9 | ||
|
|
f7816aa5cd | ||
|
|
a652ce50a9 | ||
|
|
58b726a292 | ||
|
|
1d8cf6a7f5 | ||
|
|
2c3ad380ef | ||
|
|
0e7874aacf | ||
|
|
8638d82ad3 | ||
|
|
f3d6a1f99d | ||
|
|
fa036f5807 | ||
|
|
a931f8a69f | ||
|
|
a491c9a4a0 | ||
|
|
2aafb6369c | ||
|
|
ef8253044e | ||
|
|
c1feeb72ee | ||
|
|
21560cd6cc | ||
|
|
bda2b9b0b8 | ||
|
|
4630de9616 | ||
|
|
7e83180e50 | ||
|
|
e60eed49c7 | ||
|
|
74cfc94b4c | ||
|
|
213c55c7af | ||
|
|
c066fa5e27 | ||
|
|
2741ecb968 | ||
|
|
7965c29425 | ||
|
|
d2cbab70a9 | ||
|
|
16381a1aef | ||
|
|
b92e08b850 | ||
|
|
eab470c67f | ||
|
|
7f11659d95 | ||
|
|
03dec07cbe | ||
|
|
554c696ee6 | ||
|
|
093f8a39fe | ||
|
|
8a1663f136 | ||
|
|
251d2dde97 | ||
|
|
996542a4a5 | ||
|
|
0914d6250c | ||
|
|
3ff8e511b5 | ||
|
|
3a7b27fb45 | ||
|
|
c81d2c97f5 | ||
|
|
dae46524c4 | ||
|
|
3c6386f318 | ||
|
|
1400a8806d | ||
|
|
e3f33f5a61 | ||
|
|
e6f4b88cf3 | ||
|
|
b788464487 | ||
|
|
e29717ec6c | ||
|
|
5d7e23092f | ||
|
|
9921d51451 | ||
|
|
213620cb29 | ||
|
|
bdc4aade0f | ||
|
|
b2300dbf41 | ||
|
|
44289d30f9 | ||
|
|
260fb88f85 | ||
|
|
119cdf6f09 | ||
|
|
c8d30fd214 | ||
|
|
7e9e528d3b | ||
|
|
8554c0d9cb | ||
|
|
22cc34b4fe | ||
|
|
990785ebfc | ||
|
|
957be99401 | ||
|
|
4bcde25e29 | ||
|
|
1d70f36e7d | ||
|
|
cc0a448bc8 | ||
|
|
c9e977baea | ||
|
|
6cb9a46cd4 | ||
|
|
eef379277a | ||
|
|
41fef47684 | ||
|
|
fcda6faf3d | ||
|
|
79bbf9c50b | ||
|
|
43d2f2804b | ||
|
|
fa62f3f66a | ||
|
|
229d91fe40 | ||
|
|
2673d1eee4 | ||
|
|
e59fd1118f | ||
|
|
c1fd33b152 | ||
|
|
2f58c8676f | ||
|
|
defc448304 | ||
|
|
3ec3358728 |
@@ -9,15 +9,15 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '5.2.4'
|
||||
majorVersion: '5.3.3'
|
||||
minorVersion: $[counter('minorVersion', 2000)]
|
||||
radarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(radarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.413'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '16.X'
|
||||
innoVersion: '6.2.0'
|
||||
innoVersion: '6.2.2'
|
||||
windowsImage: 'windows-2022'
|
||||
linuxImage: 'ubuntu-20.04'
|
||||
macImage: 'macOS-11'
|
||||
@@ -1242,6 +1242,7 @@ stages:
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
- Analyze
|
||||
- Installer
|
||||
- Unit_Test
|
||||
- Integration
|
||||
- Automation
|
||||
|
||||
2
build.sh
2
build.sh
@@ -254,7 +254,7 @@ InstallInno()
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
@@ -2,6 +2,8 @@ const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// Stage 1
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
['@babel/plugin-transform-optional-chaining', { loose }],
|
||||
|
||||
@@ -6,7 +6,7 @@ import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './HistoryEventTypeCell.css';
|
||||
|
||||
function getIconName(eventType) {
|
||||
function getIconName(eventType, data) {
|
||||
switch (eventType) {
|
||||
case 'grabbed':
|
||||
return icons.DOWNLOADING;
|
||||
@@ -17,7 +17,7 @@ function getIconName(eventType) {
|
||||
case 'downloadFailed':
|
||||
return icons.DOWNLOADING;
|
||||
case 'movieFileDeleted':
|
||||
return icons.DELETE;
|
||||
return data.reason === 'MissingFromDisk' ? icons.FILE_MISSING : icons.DELETE;
|
||||
case 'movieFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
@@ -47,7 +47,7 @@ function getTooltip(eventType, data) {
|
||||
case 'downloadFailed':
|
||||
return translate('MovieDownloadFailedTooltip');
|
||||
case 'movieFileDeleted':
|
||||
return translate('MovieFileDeletedTooltip');
|
||||
return data.reason === 'MissingFromDisk' ? translate('MovieFileMissingTooltip') : translate('MovieFileDeletedTooltip');
|
||||
case 'movieFileRenamed':
|
||||
return translate('MovieFileRenamedTooltip');
|
||||
case 'downloadIgnored':
|
||||
@@ -58,7 +58,7 @@ function getTooltip(eventType, data) {
|
||||
}
|
||||
|
||||
function HistoryEventTypeCell({ eventType, data }) {
|
||||
const iconName = getIconName(eventType);
|
||||
const iconName = getIconName(eventType, data);
|
||||
const iconKind = getIconKind(eventType);
|
||||
const tooltip = getTooltip(eventType, data);
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueFilterModal from './QueueFilterModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -307,9 +307,16 @@ class Queue extends Component {
|
||||
}
|
||||
</PageContentBody>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canChangeCategory={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.downloadClientHasPostImportCategory);
|
||||
})
|
||||
)}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
@@ -317,7 +324,7 @@ class Queue extends Component {
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ProgressBar from 'Components/ProgressBar';
|
||||
// import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
@@ -96,7 +96,9 @@ class QueueRow extends Component {
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
estimatedCompletionTime,
|
||||
added,
|
||||
timeleft,
|
||||
size,
|
||||
sizeleft,
|
||||
@@ -315,6 +317,15 @@ class QueueRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'added') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
date={added}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell
|
||||
@@ -363,6 +374,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!movie}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -392,7 +404,9 @@ QueueRow.propTypes = {
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
added: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
year: PropTypes.number,
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
isPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{translate('RemoveQueueItem', { sourceTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
230
frontend/src/Activity/Queue/RemoveQueueItemModal.tsx
Normal file
@@ -0,0 +1,230 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
interface RemovePressProps {
|
||||
remove: boolean;
|
||||
changeCategory: boolean;
|
||||
blocklist: boolean;
|
||||
skipRedownload: boolean;
|
||||
}
|
||||
|
||||
interface RemoveQueueItemModalProps {
|
||||
isOpen: boolean;
|
||||
sourceTitle: string;
|
||||
canChangeCategory: boolean;
|
||||
canIgnore: boolean;
|
||||
isPending: boolean;
|
||||
selectedCount?: number;
|
||||
onRemovePress(props: RemovePressProps): void;
|
||||
onModalClose: () => void;
|
||||
}
|
||||
|
||||
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
|
||||
type BlocklistMethod =
|
||||
| 'doNotBlocklist'
|
||||
| 'blocklistAndSearch'
|
||||
| 'blocklistOnly';
|
||||
|
||||
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore,
|
||||
canChangeCategory,
|
||||
isPending,
|
||||
selectedCount,
|
||||
onRemovePress,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const multipleSelected = selectedCount && selectedCount > 1;
|
||||
|
||||
const [removalMethod, setRemovalMethod] =
|
||||
useState<RemovalMethod>('removeFromClient');
|
||||
const [blocklistMethod, setBlocklistMethod] =
|
||||
useState<BlocklistMethod>('doNotBlocklist');
|
||||
|
||||
const { title, message } = useMemo(() => {
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
title: translate('RemoveQueueItem', { sourceTitle }),
|
||||
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedCount === 1) {
|
||||
return {
|
||||
title: translate('RemoveSelectedItem'),
|
||||
message: translate('RemoveSelectedItemQueueMessageText'),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: translate('RemoveSelectedItems'),
|
||||
message: translate('RemoveSelectedItemsQueueMessageText', {
|
||||
selectedCount,
|
||||
}),
|
||||
};
|
||||
}, [sourceTitle, selectedCount]);
|
||||
|
||||
const removalMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'removeFromClient',
|
||||
value: translate('RemoveFromDownloadClient'),
|
||||
hint: multipleSelected
|
||||
? translate('RemoveMultipleFromDownloadClientHint')
|
||||
: translate('RemoveFromDownloadClientHint'),
|
||||
},
|
||||
{
|
||||
key: 'changeCategory',
|
||||
value: translate('ChangeCategory'),
|
||||
isDisabled: !canChangeCategory,
|
||||
hint: multipleSelected
|
||||
? translate('ChangeCategoryMultipleHint')
|
||||
: translate('ChangeCategoryHint'),
|
||||
},
|
||||
{
|
||||
key: 'ignore',
|
||||
value: multipleSelected
|
||||
? translate('IgnoreDownloads')
|
||||
: translate('IgnoreDownload'),
|
||||
isDisabled: !canIgnore,
|
||||
hint: multipleSelected
|
||||
? translate('IgnoreDownloadsHint')
|
||||
: translate('IgnoreDownloadHint'),
|
||||
},
|
||||
];
|
||||
}, [canChangeCategory, canIgnore, multipleSelected]);
|
||||
|
||||
const blocklistMethodOptions = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
key: 'doNotBlocklist',
|
||||
value: translate('DoNotBlocklist'),
|
||||
hint: translate('DoNotBlocklistHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistAndSearch',
|
||||
value: translate('BlocklistAndSearch'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistAndSearchMultipleHint')
|
||||
: translate('BlocklistAndSearchHint'),
|
||||
},
|
||||
{
|
||||
key: 'blocklistOnly',
|
||||
value: translate('BlocklistOnly'),
|
||||
hint: multipleSelected
|
||||
? translate('BlocklistMultipleOnlyHint')
|
||||
: translate('BlocklistOnlyHint'),
|
||||
},
|
||||
];
|
||||
}, [multipleSelected]);
|
||||
|
||||
const handleRemovalMethodChange = useCallback(
|
||||
({ value }: { value: RemovalMethod }) => {
|
||||
setRemovalMethod(value);
|
||||
},
|
||||
[setRemovalMethod]
|
||||
);
|
||||
|
||||
const handleBlocklistMethodChange = useCallback(
|
||||
({ value }: { value: BlocklistMethod }) => {
|
||||
setBlocklistMethod(value);
|
||||
},
|
||||
[setBlocklistMethod]
|
||||
);
|
||||
|
||||
const handleConfirmRemove = useCallback(() => {
|
||||
onRemovePress({
|
||||
remove: removalMethod === 'removeFromClient',
|
||||
changeCategory: removalMethod === 'changeCategory',
|
||||
blocklist: blocklistMethod !== 'doNotBlocklist',
|
||||
skipRedownload: blocklistMethod === 'blocklistOnly',
|
||||
});
|
||||
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
}, [
|
||||
removalMethod,
|
||||
blocklistMethod,
|
||||
setRemovalMethod,
|
||||
setBlocklistMethod,
|
||||
onRemovePress,
|
||||
]);
|
||||
|
||||
const handleModalClose = useCallback(() => {
|
||||
setRemovalMethod('removeFromClient');
|
||||
setBlocklistMethod('doNotBlocklist');
|
||||
|
||||
onModalClose();
|
||||
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
|
||||
<ModalContent onModalClose={handleModalClose}>
|
||||
<ModalHeader>{title}</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>{message}</div>
|
||||
|
||||
{isPending ? null : (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="removalMethod"
|
||||
value={removalMethod}
|
||||
values={removalMethodOptions}
|
||||
isDisabled={!canChangeCategory && !canIgnore}
|
||||
helpTextWarning={translate(
|
||||
'RemoveQueueItemRemovalMethodHelpTextWarning'
|
||||
)}
|
||||
onChange={handleRemovalMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{multipleSelected
|
||||
? translate('BlocklistReleases')
|
||||
: translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="blocklistMethod"
|
||||
value={blocklistMethod}
|
||||
values={blocklistMethodOptions}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={handleBlocklistMethodChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={handleModalClose}>{translate('Close')}</Button>
|
||||
|
||||
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default RemoveQueueItemModal;
|
||||
@@ -1,174 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RemoveQueueItemsModal.css';
|
||||
|
||||
class RemoveQueueItemsModal extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blocklist: false,
|
||||
skipRedownload: false
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
};
|
||||
|
||||
onBlocklistChange = ({ value }) => {
|
||||
this.setState({ blocklist: value });
|
||||
};
|
||||
|
||||
onSkipRedownloadChange = ({ value }) => {
|
||||
this.setState({ skipRedownload: value });
|
||||
};
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
};
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore,
|
||||
allPending
|
||||
} = this.props;
|
||||
|
||||
const { remove, blocklist, skipRedownload } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={this.onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
|
||||
</div>
|
||||
|
||||
{
|
||||
allPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blocklist"
|
||||
value={blocklist}
|
||||
helpText={translate('BlocklistReleaseHelpText')}
|
||||
onChange={this.onBlocklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
blocklist ?
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('SkipRedownload')}</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="skipRedownload"
|
||||
value={skipRedownload}
|
||||
helpText={translate('SkipRedownloadHelpText')}
|
||||
onChange={this.onSkipRedownloadChange}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
{translate('Remove')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
allPending: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RemoveQueueItemsModal;
|
||||
@@ -3,10 +3,13 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearAddMovie, lookupMovie } from 'Store/Actions/addMovieActions';
|
||||
import { clearMovieFiles, fetchMovieFiles } from 'Store/Actions/movieFileActions';
|
||||
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchImportExclusions } from 'Store/Actions/Settings/importExclusions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
import AddNewMovie from './AddNewMovie';
|
||||
|
||||
@@ -35,7 +38,9 @@ const mapDispatchToProps = {
|
||||
fetchRootFolders,
|
||||
fetchImportExclusions,
|
||||
fetchQueueDetails,
|
||||
clearQueueDetails
|
||||
clearQueueDetails,
|
||||
fetchMovieFiles,
|
||||
clearMovieFiles
|
||||
};
|
||||
|
||||
class AddNewMovieConnector extends Component {
|
||||
@@ -55,6 +60,20 @@ class AddNewMovieConnector extends Component {
|
||||
this.props.fetchQueueDetails();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
if (hasDifferentItems(prevProps.items, items)) {
|
||||
const movieIds = selectUniqueIds(items, 'internalId');
|
||||
|
||||
if (movieIds.length) {
|
||||
this.props.fetchMovieFiles({ movieId: movieIds });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._movieLookupTimeout) {
|
||||
clearTimeout(this._movieLookupTimeout);
|
||||
@@ -62,6 +81,7 @@ class AddNewMovieConnector extends Component {
|
||||
|
||||
this.props.clearAddMovie();
|
||||
this.props.clearQueueDetails();
|
||||
this.props.clearMovieFiles();
|
||||
}
|
||||
|
||||
//
|
||||
@@ -107,12 +127,15 @@ class AddNewMovieConnector extends Component {
|
||||
|
||||
AddNewMovieConnector.propTypes = {
|
||||
term: PropTypes.string,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
lookupMovie: PropTypes.func.isRequired,
|
||||
clearAddMovie: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
fetchImportExclusions: PropTypes.func.isRequired,
|
||||
fetchQueueDetails: PropTypes.func.isRequired,
|
||||
clearQueueDetails: PropTypes.func.isRequired
|
||||
clearQueueDetails: PropTypes.func.isRequired,
|
||||
fetchMovieFiles: PropTypes.func.isRequired,
|
||||
clearMovieFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AddNewMovieConnector);
|
||||
|
||||
@@ -85,8 +85,13 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.studio,
|
||||
.genres {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.links {
|
||||
margin-left: 8px;
|
||||
margin-left: 5px;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ interface CssExports {
|
||||
'certification': string;
|
||||
'content': string;
|
||||
'exclusionIcon': string;
|
||||
'genres': string;
|
||||
'icons': string;
|
||||
'links': string;
|
||||
'overlay': string;
|
||||
@@ -14,6 +15,7 @@ interface CssExports {
|
||||
'runtime': string;
|
||||
'searchResult': string;
|
||||
'statusContainer': string;
|
||||
'studio': string;
|
||||
'title': string;
|
||||
'titleContainer': string;
|
||||
'titleRow': string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import ImdbRating from 'Components/ImdbRating';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TmdbRating from 'Components/TmdbRating';
|
||||
@@ -61,6 +62,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
titleSlug,
|
||||
year,
|
||||
studio,
|
||||
genres,
|
||||
status,
|
||||
overview,
|
||||
ratings,
|
||||
@@ -73,9 +75,9 @@ class AddNewMovieSearchResult extends Component {
|
||||
colorImpairedMode,
|
||||
id,
|
||||
monitored,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
movieFile,
|
||||
queueItem,
|
||||
runtime,
|
||||
movieRuntimeFormat,
|
||||
certification
|
||||
@@ -85,6 +87,8 @@ class AddNewMovieSearchResult extends Component {
|
||||
isNewAddMovieModalOpen
|
||||
} = this.state;
|
||||
|
||||
const hasMovieFile = !!movieFile;
|
||||
|
||||
const linkProps = isExistingMovie ? { to: `/movie/${titleSlug}` } : { onPress: this.onPress };
|
||||
const posterWidth = 167;
|
||||
const posterHeight = 250;
|
||||
@@ -123,7 +127,7 @@ class AddNewMovieSearchResult extends Component {
|
||||
movieId={existingMovieId}
|
||||
movieFile={movieFile}
|
||||
monitored={monitored}
|
||||
hasFile={hasFile}
|
||||
hasFile={hasMovieFile}
|
||||
status={status}
|
||||
width={posterWidth}
|
||||
detailedProgressBar={true}
|
||||
@@ -197,13 +201,46 @@ class AddNewMovieSearchResult extends Component {
|
||||
/>
|
||||
</Label>
|
||||
|
||||
{
|
||||
ratings.imdb ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<ImdbRating
|
||||
ratings={ratings}
|
||||
iconSize={13}
|
||||
/>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!!studio &&
|
||||
<Label size={sizes.LARGE}>
|
||||
{studio}
|
||||
<Icon
|
||||
name={icons.STUDIO}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.studio}>
|
||||
{studio}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
|
||||
{
|
||||
genres.length > 0 ?
|
||||
<Label size={sizes.LARGE}>
|
||||
<Icon
|
||||
name={icons.GENRE}
|
||||
size={13}
|
||||
/>
|
||||
|
||||
<span className={styles.genres}>
|
||||
{genres.slice(0, 3).join(', ')}
|
||||
</span>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
<Tooltip
|
||||
anchor={
|
||||
<Label
|
||||
@@ -215,15 +252,15 @@ class AddNewMovieSearchResult extends Component {
|
||||
/>
|
||||
|
||||
<span className={styles.links}>
|
||||
Links
|
||||
{translate('Links')}
|
||||
</span>
|
||||
</Label>
|
||||
}
|
||||
tooltip={
|
||||
<MovieDetailsLinks
|
||||
tmdbId={tmdbId}
|
||||
youTubeTrailerId={youTubeTrailerId}
|
||||
imdbId={imdbId}
|
||||
youTubeTrailerId={youTubeTrailerId}
|
||||
/>
|
||||
}
|
||||
canFlip={true}
|
||||
@@ -234,9 +271,10 @@ class AddNewMovieSearchResult extends Component {
|
||||
{
|
||||
isExistingMovie && isSmallScreen &&
|
||||
<MovieStatusLabel
|
||||
hasMovieFiles={hasFile}
|
||||
hasMovieFiles={hasMovieFile}
|
||||
monitored={monitored}
|
||||
isAvailable={isAvailable}
|
||||
queueItem={queueItem}
|
||||
id={id}
|
||||
useLabel={true}
|
||||
colorImpairedMode={colorImpairedMode}
|
||||
@@ -273,6 +311,7 @@ AddNewMovieSearchResult.propTypes = {
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
studio: PropTypes.string,
|
||||
genres: PropTypes.arrayOf(PropTypes.string),
|
||||
status: PropTypes.string.isRequired,
|
||||
overview: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
@@ -283,15 +322,18 @@ AddNewMovieSearchResult.propTypes = {
|
||||
isExclusionMovie: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
id: PropTypes.number,
|
||||
queueItems: PropTypes.arrayOf(PropTypes.object),
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
isAvailable: PropTypes.bool.isRequired,
|
||||
movieFile: PropTypes.object,
|
||||
queueItem: PropTypes.object,
|
||||
colorImpairedMode: PropTypes.bool,
|
||||
runtime: PropTypes.number.isRequired,
|
||||
movieRuntimeFormat: PropTypes.string.isRequired,
|
||||
certification: PropTypes.string
|
||||
};
|
||||
|
||||
AddNewMovieSearchResult.defaultProps = {
|
||||
genres: []
|
||||
};
|
||||
|
||||
export default AddNewMovieSearchResult;
|
||||
|
||||
@@ -10,14 +10,21 @@ function createMapStateToProps() {
|
||||
createExistingMovieSelector(),
|
||||
createExclusionMovieSelector(),
|
||||
createDimensionsSelector(),
|
||||
(state) => state.queue.details.items,
|
||||
(state) => state.movieFiles.items,
|
||||
(state, { internalId }) => internalId,
|
||||
(state) => state.settings.ui.item.movieRuntimeFormat,
|
||||
(isExistingMovie, isExclusionMovie, dimensions, internalId, movieRuntimeFormat) => {
|
||||
(isExistingMovie, isExclusionMovie, dimensions, queueItems, movieFiles, internalId, movieRuntimeFormat) => {
|
||||
const queueItem = queueItems.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
const movieFile = movieFiles.find((item) => internalId > 0 && item.movieId === internalId);
|
||||
|
||||
return {
|
||||
existingMovieId: internalId,
|
||||
isExistingMovie,
|
||||
isExclusionMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
queueItem,
|
||||
movieFile,
|
||||
movieRuntimeFormat
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
.contentContainer {
|
||||
z-index: $popperZIndex;
|
||||
margin-top: 4px;
|
||||
/* 400px container witdh with 8px padding on each side */
|
||||
/* 400px container width with 8px padding on each side */
|
||||
width: 384px;
|
||||
}
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class ImportMovieSelectFolder extends Component {
|
||||
className={styles.addErrorAlert}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{translate('UnableToAddRootFolder')}
|
||||
{translate('AddRootFolderError')}
|
||||
|
||||
<ul>
|
||||
{
|
||||
|
||||
@@ -44,7 +44,16 @@ export interface CustomFilter {
|
||||
filers: PropertyFilter[];
|
||||
}
|
||||
|
||||
export interface AppSectionState {
|
||||
dimensions: {
|
||||
isSmallScreen: boolean;
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
app: AppSectionState;
|
||||
calendar: CalendarAppState;
|
||||
commands: CommandAppState;
|
||||
history: HistoryAppState;
|
||||
|
||||
@@ -147,7 +147,7 @@ class AgendaEvent extends Component {
|
||||
className={styles.statusIcon}
|
||||
name={icons.MOVIE_FILE}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('QualityCutoffHasNotBeenMet')}
|
||||
title={translate('QualityCutoffNotMet')}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -33,9 +33,7 @@ class Calendar extends Component {
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadTheCalendar')}
|
||||
</Alert>
|
||||
<Alert kind={kinds.DANGER}>{translate('CalendarLoadError')}</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -55,7 +55,7 @@ class CalendarConnector extends Component {
|
||||
gotoCalendarToday
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
registerPagePopulator(this.repopulate, ['movieFileUpdated', 'movieFileDeleted']);
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchCalendar();
|
||||
|
||||
@@ -104,7 +104,7 @@ class CalendarPage extends Component {
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('iCalLink')}
|
||||
label={translate('ICalLink')}
|
||||
iconName={icons.CALENDAR}
|
||||
onPress={this.onGetCalendarLinkPress}
|
||||
/>
|
||||
@@ -112,7 +112,7 @@ class CalendarPage extends Component {
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('RSSSync')}
|
||||
label={translate('RssSync')}
|
||||
iconName={icons.RSS}
|
||||
isSpinning={isRssSyncExecuting}
|
||||
onPress={onRssSyncPress}
|
||||
@@ -180,7 +180,7 @@ class CalendarPage extends Component {
|
||||
|
||||
{
|
||||
!movieError && movieIsPopulated && !hasMovie &&
|
||||
<NoMovie />
|
||||
<NoMovie totalItems={0} />
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -48,6 +48,10 @@ $fullColorGradient: rgba(244, 245, 246, 0.2);
|
||||
.statusContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:global(.fullColor) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
|
||||
@@ -76,12 +76,18 @@ class CalendarEvent extends Component {
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className={styles.statusContainer}>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.statusContainer,
|
||||
fullColorEvents && 'fullColor'
|
||||
)}
|
||||
>
|
||||
{
|
||||
queueItem ?
|
||||
<span className={styles.statusIcon}>
|
||||
<CalendarEventQueueDetails
|
||||
{...queueItem}
|
||||
fullColorEvents={fullColorEvents}
|
||||
/>
|
||||
</span> :
|
||||
null
|
||||
@@ -98,12 +104,14 @@ class CalendarEvent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
showCutoffUnmetIcon && !!movieFile && movieFile.qualityCutoffNotMet ?
|
||||
showCutoffUnmetIcon &&
|
||||
!!movieFile &&
|
||||
movieFile.qualityCutoffNotMet ?
|
||||
<Icon
|
||||
className={styles.statusIcon}
|
||||
name={icons.MOVIE_FILE}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('QualityCutoffHasNotBeenMet')}
|
||||
title={translate('QualityCutoffNotMet')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
@@ -126,7 +126,7 @@ class CalendarHeader extends Component {
|
||||
isDisabled={view === calendarViews.AGENDA}
|
||||
onPress={onTodayPress}
|
||||
>
|
||||
Today
|
||||
{translate('Today')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -20,10 +20,11 @@ function Legend(props) {
|
||||
if (showCutoffUnmetIcon) {
|
||||
iconsToShow.push(
|
||||
<LegendIconItem
|
||||
name={translate('CutoffUnmet')}
|
||||
name={translate('CutoffNotMet')}
|
||||
icon={icons.MOVIE_FILE}
|
||||
kind={fullColorEvents ? kinds.DEFAULT : kinds.WARNING}
|
||||
tooltip={translate('QualityOrLangCutoffHasNotBeenMet')}
|
||||
kind={kinds.WARNING}
|
||||
fullColorEvents={fullColorEvents}
|
||||
tooltip={translate('QualityCutoffNotMet')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,4 +4,8 @@
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
|
||||
&:global(.fullColorEvents) {
|
||||
filter: var(--calendarFullColorFilter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
@@ -6,9 +7,9 @@ import styles from './LegendIconItem.css';
|
||||
function LegendIconItem(props) {
|
||||
const {
|
||||
name,
|
||||
fullColorEvents,
|
||||
icon,
|
||||
kind,
|
||||
darken,
|
||||
tooltip
|
||||
} = props;
|
||||
|
||||
@@ -18,9 +19,11 @@ function LegendIconItem(props) {
|
||||
title={tooltip}
|
||||
>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
className={classNames(
|
||||
styles.icon,
|
||||
fullColorEvents && 'fullColorEvents'
|
||||
)}
|
||||
name={icon}
|
||||
darken={darken}
|
||||
kind={kind}
|
||||
/>
|
||||
|
||||
@@ -31,14 +34,10 @@ function LegendIconItem(props) {
|
||||
|
||||
LegendIconItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
fullColorEvents: PropTypes.bool.isRequired,
|
||||
icon: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
darken: PropTypes.bool.isRequired,
|
||||
tooltip: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
LegendIconItem.defaultProps = {
|
||||
darken: false
|
||||
};
|
||||
|
||||
export default LegendIconItem;
|
||||
|
||||
@@ -135,7 +135,7 @@ class CalendarOptionsModalContent extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="showCutoffUnmetIcon"
|
||||
value={showCutoffUnmetIcon}
|
||||
helpText={translate('ShowCutoffUnmetIconHelpText')}
|
||||
helpText={translate('IconForCutoffUnmetHelpText')}
|
||||
onChange={this.onOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -177,7 +177,7 @@ class CalendarOptionsModalContent extends Component {
|
||||
values={weekColumnOptions}
|
||||
value={calendarWeekColumnHeader}
|
||||
onChange={this.onGlobalInputChange}
|
||||
helpText={translate('SettingsWeekColumnHeaderHelpText')}
|
||||
helpText={translate('WeekColumnHeaderHelpText')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ class CalendarLinkModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('RadarrCalendarFeed')}
|
||||
{translate('CalendarFeed')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -121,19 +121,19 @@ class CalendarLinkModalContent extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="unmonitored"
|
||||
value={unmonitored}
|
||||
helpText={translate('UnmonitoredHelpText')}
|
||||
helpText={translate('ICalIncludeUnmonitoredMoviesHelpText')}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('ShowAsAllDayEvents')}</FormLabel>
|
||||
<FormLabel>{translate('ICalShowAsAllDayEvents')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="asAllDay"
|
||||
value={asAllDay}
|
||||
helpText={translate('AsAllDayHelpText')}
|
||||
helpText={translate('ICalShowAsAllDayEventsHelpText')}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -145,7 +145,7 @@ class CalendarLinkModalContent extends Component {
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
value={tags}
|
||||
helpText={translate('TagsHelpText')}
|
||||
helpText={translate('ICalTagsMoviesHelpText')}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -160,7 +160,7 @@ class CalendarLinkModalContent extends Component {
|
||||
name="iCalHttpUrl"
|
||||
value={iCalHttpUrl}
|
||||
readOnly={true}
|
||||
helpText={translate('ICalHttpUrlHelpText')}
|
||||
helpText={translate('ICalFeedHelpText')}
|
||||
buttons={[
|
||||
<ClipboardButton
|
||||
key="copy"
|
||||
|
||||
@@ -74,11 +74,7 @@ CollectionMovieLabel.propTypes = {
|
||||
|
||||
CollectionMovieLabel.defaultProps = {
|
||||
isSaving: false,
|
||||
statistics: {
|
||||
episodeFileCount: 0,
|
||||
totalEpisodeCount: 0,
|
||||
percentOfEpisodes: 0
|
||||
}
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default CollectionMovieLabel;
|
||||
|
||||
@@ -30,22 +30,24 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
|
||||
@@ -267,6 +267,7 @@ FormInputGroup.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.any,
|
||||
values: PropTypes.arrayOf(PropTypes.any),
|
||||
isDisabled: PropTypes.bool,
|
||||
type: PropTypes.string.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
min: PropTypes.number,
|
||||
|
||||
@@ -12,18 +12,10 @@
|
||||
|
||||
.info {
|
||||
color: var(--infoColor);
|
||||
|
||||
&:global(.darken) {
|
||||
color: color(var(--infoColor) shade(30%));
|
||||
}
|
||||
}
|
||||
|
||||
.pink {
|
||||
color: var(--pink);
|
||||
|
||||
&:global(.darken) {
|
||||
color: color(var(--pink) shade(30%));
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
|
||||
@@ -18,7 +18,6 @@ class Icon extends PureComponent {
|
||||
kind,
|
||||
size,
|
||||
title,
|
||||
darken,
|
||||
isSpinning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -27,8 +26,7 @@ class Icon extends PureComponent {
|
||||
<FontAwesomeIcon
|
||||
className={classNames(
|
||||
className,
|
||||
styles[kind],
|
||||
darken && 'darken'
|
||||
styles[kind]
|
||||
)}
|
||||
icon={name}
|
||||
spin={isSpinning}
|
||||
@@ -61,7 +59,6 @@ Icon.propTypes = {
|
||||
kind: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
|
||||
darken: PropTypes.bool.isRequired,
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
fixedWidth: PropTypes.bool.isRequired
|
||||
};
|
||||
@@ -69,7 +66,6 @@ Icon.propTypes = {
|
||||
Icon.defaultProps = {
|
||||
kind: kinds.DEFAULT,
|
||||
size: 14,
|
||||
darken: false,
|
||||
isSpinning: false,
|
||||
fixedWidth: false
|
||||
};
|
||||
|
||||
@@ -40,18 +40,26 @@ class FilterMenuContent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
customFilters.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
customFilters.length > 0 ?
|
||||
<MenuItemSeparator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -167,7 +167,7 @@ class SignalRConnector extends Component {
|
||||
const resource = body.resource;
|
||||
const status = resource.status;
|
||||
|
||||
// Both sucessful and failed commands need to be
|
||||
// Both successful and failed commands need to be
|
||||
// completed, otherwise they spin until they timeout.
|
||||
|
||||
if (status === 'completed' || status === 'failed') {
|
||||
@@ -187,6 +187,8 @@ class SignalRConnector extends Component {
|
||||
repopulatePage('movieFileUpdated');
|
||||
} else if (body.action === 'deleted') {
|
||||
this.props.dispatchRemoveItem({ section, id: body.resource.id });
|
||||
|
||||
repopulatePage('movieFileDeleted');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"start_url": "../../../../",
|
||||
"theme_color": "#3a3f51",
|
||||
"background_color": "#3a3f51",
|
||||
"display": "standalone"
|
||||
"display": "minimal-ui"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
faEye as fasEye,
|
||||
faFastBackward as fasFastBackward,
|
||||
faFastForward as fasFastForward,
|
||||
faFileCircleQuestion as fasFileCircleQuestion,
|
||||
faFileExport as fasFileExport,
|
||||
faFileInvoice as farFileInvoice,
|
||||
faFilm as fasFilm,
|
||||
@@ -159,6 +160,7 @@ export const EXPORT = fasFileExport;
|
||||
export const EXTERNAL_LINK = fasExternalLinkAlt;
|
||||
export const FATAL = fasTimesCircle;
|
||||
export const FILE = farFile;
|
||||
export const FILE_MISSING = fasFileCircleQuestion;
|
||||
export const FILM = fasFilm;
|
||||
export const FILTER = fasFilter;
|
||||
export const FLAG = fasFlag;
|
||||
|
||||
@@ -22,6 +22,10 @@
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
}
|
||||
|
||||
.quality {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.languages {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<MovieQuality quality={quality} />
|
||||
<MovieQuality quality={quality} showRevision={true} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
|
||||
@@ -50,12 +50,16 @@ class DeleteMovieModalContent extends Component {
|
||||
title,
|
||||
path,
|
||||
hasFile,
|
||||
statistics,
|
||||
deleteOptions,
|
||||
sizeOnDisk,
|
||||
onModalClose,
|
||||
onDeleteOptionChange
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const deleteFiles = this.state.deleteFiles;
|
||||
const addImportExclusion = deleteOptions.addImportExclusion;
|
||||
|
||||
@@ -151,12 +155,16 @@ class DeleteMovieModalContent extends Component {
|
||||
DeleteMovieModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
hasFile: PropTypes.bool.isRequired,
|
||||
sizeOnDisk: PropTypes.number.isRequired,
|
||||
deleteOptions: PropTypes.object.isRequired,
|
||||
onDeleteOptionChange: PropTypes.func.isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
DeleteMovieModalContent.defaultProps = {
|
||||
statistics: {}
|
||||
};
|
||||
|
||||
export default DeleteMovieModalContent;
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from '../MovieCreditPoster.css';
|
||||
|
||||
class MovieCastPoster extends Component {
|
||||
@@ -52,6 +58,7 @@ class MovieCastPoster extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
personName,
|
||||
character,
|
||||
images,
|
||||
@@ -83,15 +90,35 @@ class MovieCastPoster extends Component {
|
||||
style={contentStyle}
|
||||
>
|
||||
<div className={styles.posterContainer}>
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.action}
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={styles.controls}>
|
||||
<span className={styles.externalLinks}>
|
||||
<Popover
|
||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||
<Label
|
||||
className={styles.externalLinkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{translate('TMDb')}
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
style={elementStyle}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Label from 'Components/Label';
|
||||
import Link from 'Components/Link/Link';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import MovieHeadshot from 'Movie/MovieHeadshot';
|
||||
import EditImportListModalConnector from 'Settings/ImportLists/ImportLists/EditImportListModalConnector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from '../MovieCreditPoster.css';
|
||||
|
||||
class MovieCrewPoster extends Component {
|
||||
@@ -52,6 +58,7 @@ class MovieCrewPoster extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
tmdbId,
|
||||
personName,
|
||||
job,
|
||||
images,
|
||||
@@ -83,15 +90,35 @@ class MovieCrewPoster extends Component {
|
||||
style={contentStyle}
|
||||
>
|
||||
<div className={styles.posterContainer}>
|
||||
<div className={styles.controls}>
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.action}
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
size={20}
|
||||
onPress={importListId > 0 ? this.onEditImportListPress : this.onAddImportListPress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Label className={styles.controls}>
|
||||
<span className={styles.externalLinks}>
|
||||
<Popover
|
||||
anchor={<Icon name={icons.EXTERNAL_LINK} size={12} />}
|
||||
title={translate('Links')}
|
||||
body={
|
||||
<Link to={`https://www.themoviedb.org/person/${tmdbId}`}>
|
||||
<Label
|
||||
className={styles.externalLinkLabel}
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
{translate('TMDb')}
|
||||
</Label>
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
</Label>
|
||||
|
||||
<div
|
||||
style={elementStyle}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,29 @@ import { createSelector } from 'reselect';
|
||||
import MovieCreditPosters from '../MovieCreditPosters';
|
||||
import MovieCrewPoster from './MovieCrewPoster';
|
||||
|
||||
function crewSort(a, b) {
|
||||
const jobOrder = ['Director', 'Writer', 'Producer', 'Executive Producer', 'Director of Photography'];
|
||||
|
||||
const indexA = jobOrder.indexOf(a.job);
|
||||
const indexB = jobOrder.indexOf(b.job);
|
||||
|
||||
if (indexA === -1 && indexB === -1) {
|
||||
return 0;
|
||||
} else if (indexA === -1) {
|
||||
return 1;
|
||||
} else if (indexB === -1) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (indexA < indexB) {
|
||||
return -1;
|
||||
} else if (indexA > indexB) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.movieCredits.items,
|
||||
@@ -17,8 +40,10 @@ function createMapStateToProps() {
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const sortedCrew = crew.sort(crewSort);
|
||||
|
||||
return {
|
||||
items: _.uniqBy(crew, 'personName')
|
||||
items: _.uniqBy(sortedCrew, 'personName')
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
$hoverScale: 1.05;
|
||||
|
||||
.content {
|
||||
border-radius: '5px';
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
&:hover {
|
||||
z-index: 2;
|
||||
box-shadow: 0 0 12px var(--black);
|
||||
transition: all 200ms ease-in;
|
||||
|
||||
.controls {
|
||||
opacity: 0.9;
|
||||
transition: opacity 200ms linear 150ms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,13 +48,13 @@ $hoverScale: 1.05;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
||||
.controls {
|
||||
.toggleMonitoredContainer {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.action {
|
||||
.monitorToggleButton {
|
||||
composes: toggleButton from '~Components/MonitorToggleButton.css';
|
||||
|
||||
width: 25px;
|
||||
@@ -61,8 +65,39 @@ $hoverScale: 1.05;
|
||||
}
|
||||
}
|
||||
|
||||
.controls {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
z-index: 3;
|
||||
border-radius: 4px;
|
||||
background-color: #707070;
|
||||
color: var(--white);
|
||||
font-size: $smallFontSize;
|
||||
opacity: 0;
|
||||
transition: opacity 0;
|
||||
}
|
||||
|
||||
.action {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
&:hover {
|
||||
color: var(--iconButtonHoverLightColor);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.container {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.externalLinks {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.externalLinkLabel {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,14 @@ interface CssExports {
|
||||
'container': string;
|
||||
'content': string;
|
||||
'controls': string;
|
||||
'externalLinkLabel': string;
|
||||
'externalLinks': string;
|
||||
'monitorToggleButton': string;
|
||||
'overlayTitle': string;
|
||||
'poster': string;
|
||||
'posterContainer': string;
|
||||
'title': string;
|
||||
'toggleMonitoredContainer': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -9,3 +9,9 @@
|
||||
.container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
--swiper-navigation-color: var(--white);
|
||||
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ interface CssExports {
|
||||
'container': string;
|
||||
'grid': string;
|
||||
'movie': string;
|
||||
'sliderContainer': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
|
||||
@@ -14,24 +14,6 @@ import 'swiper/css/navigation';
|
||||
const columnPadding = parseInt(dimensions.movieIndexColumnPadding);
|
||||
const columnPaddingSmallScreen = parseInt(dimensions.movieIndexColumnPaddingSmallScreen);
|
||||
|
||||
const additionalColumnCount = {
|
||||
small: 3,
|
||||
medium: 2,
|
||||
large: 1
|
||||
};
|
||||
|
||||
function calculateColumnWidth(width, posterSize, isSmallScreen) {
|
||||
const maxiumColumnWidth = isSmallScreen ? 172 : 182;
|
||||
const columns = Math.floor(width / maxiumColumnWidth);
|
||||
const remainder = width % maxiumColumnWidth;
|
||||
|
||||
if (remainder === 0 && posterSize === 'large') {
|
||||
return maxiumColumnWidth;
|
||||
}
|
||||
|
||||
return Math.floor(width / (columns + additionalColumnCount[posterSize]));
|
||||
}
|
||||
|
||||
function calculateRowHeight(posterHeight, isSmallScreen) {
|
||||
const titleHeight = 19;
|
||||
const characterHeight = 19;
|
||||
@@ -46,10 +28,6 @@ function calculateRowHeight(posterHeight, isSmallScreen) {
|
||||
return heights.reduce((acc, height) => acc + height, 0);
|
||||
}
|
||||
|
||||
function calculatePosterHeight(posterWidth) {
|
||||
return Math.ceil((250 / 170) * posterWidth);
|
||||
}
|
||||
|
||||
class MovieCreditPosters extends Component {
|
||||
|
||||
//
|
||||
@@ -66,39 +44,16 @@ class MovieCreditPosters extends Component {
|
||||
posterHeight: 238,
|
||||
rowHeight: calculateRowHeight(238, props.isSmallScreen)
|
||||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
calculateGrid = (width = this.state.width, isSmallScreen) => {
|
||||
|
||||
const padding = isSmallScreen ? columnPaddingSmallScreen : columnPadding;
|
||||
const columnWidth = calculateColumnWidth(width, 'small', isSmallScreen);
|
||||
const columnCount = Math.max(Math.floor(width / columnWidth), 1);
|
||||
const posterWidth = columnWidth - padding;
|
||||
const posterHeight = calculatePosterHeight(posterWidth);
|
||||
const rowHeight = calculateRowHeight(posterHeight, isSmallScreen);
|
||||
|
||||
this.setState({
|
||||
width,
|
||||
columnWidth,
|
||||
columnCount,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
rowHeight
|
||||
});
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
itemComponent
|
||||
itemComponent,
|
||||
isSmallScreen
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
@@ -113,14 +68,13 @@ class MovieCreditPosters extends Component {
|
||||
<Swiper
|
||||
slidesPerView='auto'
|
||||
spaceBetween={10}
|
||||
slidesPerGroup={3}
|
||||
slidesPerGroup={isSmallScreen ? 1 : 3}
|
||||
navigation={true}
|
||||
loop={false}
|
||||
loopFillGroupWithBlank={true}
|
||||
className="mySwiper"
|
||||
modules={[Navigation]}
|
||||
onInit={(swiper) => {
|
||||
swiper.params.navigation.prevEl = this._swiperPrevRef;
|
||||
swiper.params.navigation.nextEl = this._swiperNextRef;
|
||||
swiper.navigation.init();
|
||||
swiper.navigation.update();
|
||||
}}
|
||||
|
||||
@@ -238,7 +238,7 @@ class MovieDetails extends Component {
|
||||
certification,
|
||||
ratings,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
statistics,
|
||||
qualityProfileId,
|
||||
monitored,
|
||||
studio,
|
||||
@@ -267,6 +267,10 @@ class MovieDetails extends Component {
|
||||
movieRuntimeFormat
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isEditMovieModalOpen,
|
||||
@@ -320,8 +324,8 @@ class MovieDetails extends Component {
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label={translate('ManualImport')}
|
||||
iconName={icons.INTERACTIVE}
|
||||
label={translate('ManageFiles')}
|
||||
iconName={icons.MOVIE_FILE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
@@ -704,6 +708,7 @@ class MovieDetails extends Component {
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
movieId={id}
|
||||
modalTitle={translate('ManageFiles')}
|
||||
folder={path}
|
||||
allowMovieChange={false}
|
||||
showFilterExistingFiles={true}
|
||||
@@ -733,7 +738,7 @@ MovieDetails.propTypes = {
|
||||
certification: PropTypes.string,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
sizeOnDisk: PropTypes.number.isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
@@ -772,9 +777,9 @@ MovieDetails.propTypes = {
|
||||
|
||||
MovieDetails.defaultProps = {
|
||||
genres: [],
|
||||
statistics: {},
|
||||
tags: [],
|
||||
isSaving: false,
|
||||
sizeOnDisk: 0
|
||||
isSaving: false
|
||||
};
|
||||
|
||||
export default MovieDetails;
|
||||
|
||||
@@ -33,14 +33,11 @@ const selectMovieFiles = createSelector(
|
||||
|
||||
const hasMovieFiles = !!items.length;
|
||||
|
||||
const sizeOnDisk = items.map((item) => item.size).reduce((prev, curr) => prev + curr, 0);
|
||||
|
||||
return {
|
||||
isMovieFilesFetching: isFetching,
|
||||
isMovieFilesPopulated: isPopulated,
|
||||
movieFilesError: error,
|
||||
hasMovieFiles,
|
||||
sizeOnDisk
|
||||
hasMovieFiles
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -104,8 +101,7 @@ function createMapStateToProps() {
|
||||
isMovieFilesFetching,
|
||||
isMovieFilesPopulated,
|
||||
movieFilesError,
|
||||
hasMovieFiles,
|
||||
sizeOnDisk
|
||||
hasMovieFiles
|
||||
} = movieFiles;
|
||||
|
||||
const {
|
||||
@@ -161,7 +157,6 @@ function createMapStateToProps() {
|
||||
movieCreditsError,
|
||||
extraFilesError,
|
||||
hasMovieFiles,
|
||||
sizeOnDisk,
|
||||
previousMovie,
|
||||
nextMovie,
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
|
||||
@@ -6,31 +6,43 @@ import MovieTitlesTableContent from './MovieTitlesTableContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { movieId }) => movieId,
|
||||
(state) => state.movies,
|
||||
(movies) => {
|
||||
return movies;
|
||||
(movieId, movies) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = movies;
|
||||
|
||||
const alternateTitles = items.find((m) => m.id === movieId)?.alternateTitles;
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
alternateTitles
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
// fetchMovies
|
||||
};
|
||||
|
||||
class MovieTitlesTableContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const movie = this.props.items.filter((obj) => {
|
||||
return obj.id === this.props.movieId;
|
||||
});
|
||||
const {
|
||||
alternateTitles,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<MovieTitlesTableContent
|
||||
{...this.props}
|
||||
items={movie[0].alternateTitles}
|
||||
{...otherProps}
|
||||
items={alternateTitles}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -38,7 +50,11 @@ class MovieTitlesTableContentConnector extends Component {
|
||||
|
||||
MovieTitlesTableContentConnector.propTypes = {
|
||||
movieId: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(MovieTitlesTableContentConnector);
|
||||
MovieTitlesTableContentConnector.defaultProps = {
|
||||
alternateTitles: []
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(MovieTitlesTableContentConnector);
|
||||
|
||||
@@ -56,7 +56,6 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: () => translate('Actions'),
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -7,8 +7,7 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
@@ -103,20 +102,11 @@ class MovieHistoryRow extends Component {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
@@ -134,6 +124,7 @@ class MovieHistoryRow extends Component {
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -17,13 +17,13 @@ function createUnoptimizedSelector() {
|
||||
createClientSideCollectionSelector('movies', 'movieIndex'),
|
||||
(movies: MoviesAppState) => {
|
||||
return movies.items.map((m) => {
|
||||
const { monitored, status, hasFile, sizeOnDisk } = m;
|
||||
const { monitored, status, hasFile, statistics } = m;
|
||||
|
||||
return {
|
||||
monitored,
|
||||
status,
|
||||
hasFile,
|
||||
sizeOnDisk,
|
||||
statistics,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -44,16 +44,20 @@ export default function MovieIndexFooter() {
|
||||
let monitored = 0;
|
||||
let totalFileSize = 0;
|
||||
|
||||
movies.forEach((s) => {
|
||||
if (s.hasFile) {
|
||||
movies.forEach((m) => {
|
||||
const { statistics = { sizeOnDisk: 0 } } = m;
|
||||
|
||||
const { sizeOnDisk = 0 } = statistics;
|
||||
|
||||
if (m.hasFile) {
|
||||
movieFiles += 1;
|
||||
}
|
||||
|
||||
if (s.monitored) {
|
||||
if (m.monitored) {
|
||||
monitored++;
|
||||
}
|
||||
|
||||
totalFileSize += s.sizeOnDisk;
|
||||
totalFileSize += sizeOnDisk;
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSelect } from 'App/SelectContext';
|
||||
import ClientSideCollectionAppState from 'App/State/ClientSideCollectionAppState';
|
||||
import MoviesAppState, { MovieIndexAppState } from 'App/State/MoviesAppState';
|
||||
import { MOVIE_SEARCH } from 'Commands/commandNames';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createMovieClientSideCollectionItemsSelector from 'Store/Selectors/createMovieClientSideCollectionItemsSelector';
|
||||
@@ -21,11 +22,12 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
||||
const isSearching = useSelector(createCommandExecutingSelector(MOVIE_SEARCH));
|
||||
const {
|
||||
items,
|
||||
totalItems,
|
||||
}: MoviesAppState & MovieIndexAppState & ClientSideCollectionAppState =
|
||||
useSelector(createMovieClientSideCollectionItemsSelector('movieIndex'));
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false);
|
||||
|
||||
const { isSelectMode, selectedFilterKey } = props;
|
||||
const [selectState] = useSelect();
|
||||
const { selectedState } = selectState;
|
||||
@@ -50,6 +52,8 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
||||
: translate('SearchAll');
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
setIsConfirmModalOpen(false);
|
||||
|
||||
dispatch(
|
||||
executeCommand({
|
||||
name: MOVIE_SEARCH,
|
||||
@@ -58,14 +62,36 @@ function MovieIndexSearchButton(props: MovieIndexSearchButtonProps) {
|
||||
);
|
||||
}, [dispatch, moviesToSearch]);
|
||||
|
||||
const onConfirmPress = useCallback(() => {
|
||||
setIsConfirmModalOpen(true);
|
||||
}, [setIsConfirmModalOpen]);
|
||||
|
||||
const onConfirmModalClose = useCallback(() => {
|
||||
setIsConfirmModalOpen(false);
|
||||
}, [setIsConfirmModalOpen]);
|
||||
|
||||
return (
|
||||
<PageToolbarButton
|
||||
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||
isSpinning={isSearching}
|
||||
isDisabled={!totalItems}
|
||||
iconName={icons.SEARCH}
|
||||
onPress={onPress}
|
||||
/>
|
||||
<>
|
||||
<PageToolbarButton
|
||||
label={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||
isSpinning={isSearching}
|
||||
isDisabled={!items.length}
|
||||
iconName={icons.SEARCH}
|
||||
onPress={moviesToSearch.length > 5 ? onConfirmPress : onPress}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||
message={translate('SearchMoviesConfirmationMessageText', {
|
||||
count: moviesToSearch.length,
|
||||
})}
|
||||
confirmLabel={isSelectMode ? searchSelectLabel : searchIndexLabel}
|
||||
onConfirm={onPress}
|
||||
onCancel={onConfirmModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
@@ -66,17 +67,19 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
|
||||
status,
|
||||
path,
|
||||
overview,
|
||||
statistics = {} as Statistics,
|
||||
images,
|
||||
hasFile,
|
||||
isAvailable,
|
||||
tmdbId,
|
||||
imdbId,
|
||||
studio,
|
||||
sizeOnDisk,
|
||||
added,
|
||||
youTubeTrailerId,
|
||||
} = movie;
|
||||
|
||||
const { sizeOnDisk = 0 } = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||
|
||||
@@ -16,6 +16,7 @@ import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import MovieIndexProgressBar from 'Movie/Index/ProgressBar/MovieIndexProgressBar';
|
||||
import MovieIndexPosterSelect from 'Movie/Index/Select/MovieIndexPosterSelect';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import MoviePoster from 'Movie/MoviePoster';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
@@ -75,12 +76,14 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
|
||||
path,
|
||||
movieFile,
|
||||
ratings,
|
||||
sizeOnDisk,
|
||||
statistics = {} as Statistics,
|
||||
certification,
|
||||
originalTitle,
|
||||
originalLanguage,
|
||||
} = movie;
|
||||
|
||||
const { sizeOnDisk = 0 } = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [hasPosterError, setHasPosterError] = useState(false);
|
||||
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||
|
||||
@@ -244,11 +244,15 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
|
||||
if (isSmallScreen) {
|
||||
const padding = bodyPaddingSmallScreen - 5;
|
||||
const width = window.innerWidth - padding * 2;
|
||||
const height = window.innerHeight;
|
||||
|
||||
setSize({
|
||||
width: window.innerWidth - padding * 2,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
if (width !== size.width || height !== size.height) {
|
||||
setSize({
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -256,13 +260,18 @@ export default function MovieIndexPosters(props: MovieIndexPostersProps) {
|
||||
if (current) {
|
||||
const width = current.clientWidth;
|
||||
const padding = bodyPadding - 5;
|
||||
const finalWidth = width - padding * 2;
|
||||
|
||||
if (Math.abs(size.width - finalWidth) < 20 || size.width === finalWidth) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSize({
|
||||
width: width - padding * 2,
|
||||
width: finalWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
}, [isSmallScreen, scrollerRef, bounds]);
|
||||
}, [isSmallScreen, size, scrollerRef, bounds]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentScrollerRef = scrollerRef.current as HTMLElement;
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.releaseGroups,
|
||||
.inCinemas,
|
||||
.physicalRelease,
|
||||
.digitalRelease,
|
||||
|
||||
@@ -20,6 +20,7 @@ interface CssExports {
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'qualityProfileId': string;
|
||||
'releaseGroups': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sizeOnDisk': string;
|
||||
|
||||
@@ -19,6 +19,7 @@ import DeleteMovieModal from 'Movie/Delete/DeleteMovieModal';
|
||||
import MovieDetailsLinks from 'Movie/Details/MovieDetailsLinks';
|
||||
import EditMovieModalConnector from 'Movie/Edit/EditMovieModalConnector';
|
||||
import createMovieIndexItemSelector from 'Movie/Index/createMovieIndexItemSelector';
|
||||
import { Statistics } from 'Movie/Movie';
|
||||
import MoviePopularityIndex from 'Movie/MoviePopularityIndex';
|
||||
import MovieTitleLink from 'Movie/MovieTitleLink';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
@@ -60,6 +61,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
originalLanguage,
|
||||
originalTitle,
|
||||
added,
|
||||
statistics = {} as Statistics,
|
||||
year,
|
||||
inCinemas,
|
||||
digitalRelease,
|
||||
@@ -67,7 +69,6 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
runtime,
|
||||
minimumAvailability,
|
||||
path,
|
||||
sizeOnDisk,
|
||||
genres = [],
|
||||
ratings,
|
||||
popularity,
|
||||
@@ -82,6 +83,8 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
isSaving = false,
|
||||
} = movie;
|
||||
|
||||
const { sizeOnDisk = 0, releaseGroups = [] } = statistics;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [isEditMovieModalOpen, setIsEditMovieModalOpen] = useState(false);
|
||||
const [isDeleteMovieModalOpen, setIsDeleteMovieModalOpen] = useState(false);
|
||||
@@ -380,6 +383,20 @@ function MovieIndexRow(props: MovieIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'releaseGroups') {
|
||||
const joinedReleaseGroups = releaseGroups.join(', ');
|
||||
const truncatedReleaseGroups =
|
||||
releaseGroups.length > 3
|
||||
? `${releaseGroups.slice(0, 3).join(', ')}...`
|
||||
: joinedReleaseGroups;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<span title={joinedReleaseGroups}>{truncatedReleaseGroups}</span>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'tags') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
flex: 1 0 125px;
|
||||
}
|
||||
|
||||
.releaseGroups,
|
||||
.inCinemas,
|
||||
.physicalRelease,
|
||||
.digitalRelease,
|
||||
|
||||
@@ -17,6 +17,7 @@ interface CssExports {
|
||||
'physicalRelease': string;
|
||||
'popularity': string;
|
||||
'qualityProfileId': string;
|
||||
'releaseGroups': string;
|
||||
'rottenTomatoesRating': string;
|
||||
'runtime': string;
|
||||
'sizeOnDisk': string;
|
||||
|
||||
@@ -12,6 +12,12 @@ export interface Collection {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface Statistics {
|
||||
movieFileCount: number;
|
||||
releaseGroups: string[];
|
||||
sizeOnDisk: number;
|
||||
}
|
||||
|
||||
export interface Ratings {
|
||||
imdb: object;
|
||||
tmdb: object;
|
||||
@@ -42,11 +48,11 @@ interface Movie extends ModelBase {
|
||||
runtime: number;
|
||||
minimumAvailability: string;
|
||||
path: string;
|
||||
sizeOnDisk: number;
|
||||
genres: string[];
|
||||
ratings: Ratings;
|
||||
popularity: number;
|
||||
certification: string;
|
||||
statistics: Statistics;
|
||||
tags: number[];
|
||||
images: Image[];
|
||||
movieFile: MovieFile;
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||
const revision = quality.revision;
|
||||
@@ -28,6 +29,36 @@ function getTooltip(title, quality, size, isMonitored, isCutoffNotMet) {
|
||||
return title;
|
||||
}
|
||||
|
||||
function revisionLabel(className, quality, showRevision) {
|
||||
if (!showRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (quality.revision.isRepack) {
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kinds.PRIMARY}
|
||||
title={translate('Repack')}
|
||||
>
|
||||
R
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
if (quality.revision.version && quality.revision.version > 1) {
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kinds.PRIMARY}
|
||||
title={translate('Proper')}
|
||||
>
|
||||
P
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function MovieQuality(props) {
|
||||
const {
|
||||
className,
|
||||
@@ -35,7 +66,8 @@ function MovieQuality(props) {
|
||||
quality,
|
||||
size,
|
||||
isMonitored,
|
||||
isCutoffNotMet
|
||||
isCutoffNotMet,
|
||||
showRevision
|
||||
} = props;
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
@@ -50,13 +82,15 @@ function MovieQuality(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
kind={kind}
|
||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>
|
||||
<span>
|
||||
<Label
|
||||
className={className}
|
||||
kind={kind}
|
||||
title={getTooltip(title, quality, size, isMonitored, isCutoffNotMet)}
|
||||
>
|
||||
{quality.quality.name}
|
||||
</Label>{revisionLabel(className, quality, showRevision)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,12 +100,14 @@ MovieQuality.propTypes = {
|
||||
quality: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
isMonitored: PropTypes.bool,
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
isCutoffNotMet: PropTypes.bool,
|
||||
showRevision: PropTypes.bool
|
||||
};
|
||||
|
||||
MovieQuality.defaultProps = {
|
||||
title: '',
|
||||
isMonitored: true
|
||||
isMonitored: true,
|
||||
showRevision: false
|
||||
};
|
||||
|
||||
export default MovieQuality;
|
||||
|
||||
@@ -45,7 +45,8 @@
|
||||
width: 165px;
|
||||
}
|
||||
|
||||
.releaseGroup {
|
||||
.releaseGroup,
|
||||
.dateAdded {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 120px;
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CssExports {
|
||||
'audio': string;
|
||||
'audioLanguages': string;
|
||||
'customFormatScore': string;
|
||||
'dateAdded': string;
|
||||
'download': string;
|
||||
'formats': string;
|
||||
'language': string;
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
@@ -82,6 +83,7 @@ class MovieFileEditorRow extends Component {
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
dateAdded,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
@@ -287,6 +289,16 @@ class MovieFileEditorRow extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'dateAdded') {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
className={styles.dateAdded}
|
||||
date={dateAdded}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<TableRowCell key={name} className={styles.actions}>
|
||||
@@ -354,6 +366,7 @@ MovieFileEditorRow.propTypes = {
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
mediaInfo: PropTypes.object,
|
||||
dateAdded: PropTypes.string,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './RootFolderRow.css';
|
||||
|
||||
function RootFolderRow(props) {
|
||||
const {
|
||||
id,
|
||||
path,
|
||||
accessible,
|
||||
freeSpace,
|
||||
unmappedFolders,
|
||||
onDeletePress
|
||||
} = props;
|
||||
|
||||
const isUnavailable = !accessible;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell>
|
||||
{
|
||||
isUnavailable ?
|
||||
<div className={styles.unavailablePath}>
|
||||
{path}
|
||||
|
||||
<Label
|
||||
className={styles.unavailableLabel}
|
||||
kind={kinds.DANGER}
|
||||
>
|
||||
{translate('Unavailable')}
|
||||
</Label>
|
||||
</div> :
|
||||
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`/add/import/${id}`}
|
||||
>
|
||||
{path}
|
||||
</Link>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.freeSpace}>
|
||||
{(isUnavailable || isNaN(freeSpace)) ? '-' : formatBytes(freeSpace)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.unmappedFolders}>
|
||||
{isUnavailable ? '-' : unmappedFolders.length}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
<IconButton
|
||||
title={translate('RemoveRootFolder')}
|
||||
name={icons.REMOVE}
|
||||
onPress={onDeletePress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
RootFolderRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
accessible: PropTypes.bool.isRequired,
|
||||
freeSpace: PropTypes.number,
|
||||
unmappedFolders: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
RootFolderRow.defaultProps = {
|
||||
unmappedFolders: []
|
||||
};
|
||||
|
||||
export default RootFolderRow;
|
||||
@@ -1,92 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import RootFolderRowConnector from './RootFolderRowConnector';
|
||||
|
||||
const rootFolderColumns = [
|
||||
{
|
||||
name: 'path',
|
||||
get label() {
|
||||
return translate('Path');
|
||||
},
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
get label() {
|
||||
return translate('FreeSpace');
|
||||
},
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'unmappedFolders',
|
||||
get label() {
|
||||
return translate('UnmappedFolders');
|
||||
},
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
function RootFolders(props) {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = props;
|
||||
|
||||
if (isFetching && !isPopulated) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
}
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('UnableToLoadRootFolders')}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={rootFolderColumns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((rootFolder) => {
|
||||
return (
|
||||
<RootFolderRowConnector
|
||||
key={rootFolder.id}
|
||||
id={rootFolder.id}
|
||||
path={rootFolder.path}
|
||||
accessible={rootFolder.accessible}
|
||||
freeSpace={rootFolder.freeSpace}
|
||||
unmappedFolders={rootFolder.unmappedFolders}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
RootFolders.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default RootFolders;
|
||||
@@ -49,7 +49,7 @@ function RootFolders() {
|
||||
|
||||
if (!isFetching && !!error) {
|
||||
return (
|
||||
<Alert kind={kinds.DANGER}>{translate('UnableToLoadRootFolders')}</Alert>
|
||||
<Alert kind={kinds.DANGER}>{translate('RootFoldersLoadError')}</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ function EditSpecificationModalContent(props) {
|
||||
{...otherProps}
|
||||
>
|
||||
{
|
||||
fields && fields.some((x) => x.label === 'Regular Expression') &&
|
||||
fields && fields.some((x) => x.label === translate('CustomFormatsSpecificationRegularExpression')) &&
|
||||
<Alert kind={kinds.INFO}>
|
||||
<div>
|
||||
<div dangerouslySetInnerHTML={{ __html: translate('ThisConditionMatchesUsingRegularExpressions', ['<code>\\^$.|?*+()[{</code>', '<code>\\</code>']) }} />
|
||||
|
||||
@@ -133,7 +133,7 @@ class EditDownloadClientModalContent extends Component {
|
||||
<FormInputGroup
|
||||
type={inputTypes.NUMBER}
|
||||
name="priority"
|
||||
helpText={translate('PriorityHelpText')}
|
||||
helpText={translate('DownloadClientPriorityHelpText')}
|
||||
min={1}
|
||||
max={50}
|
||||
{...priority}
|
||||
|
||||
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteDownloadClients,
|
||||
bulkEditDownloadClients,
|
||||
setManageDownloadClientsSort,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
@@ -80,6 +82,8 @@ const COLUMNS = [
|
||||
|
||||
interface ManageDownloadClientsModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageDownloadClientsModalContent(
|
||||
@@ -94,6 +98,8 @@ function ManageDownloadClientsModalContent(
|
||||
isSaving,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
}: DownloadClientAppState = useSelector(
|
||||
createClientSideCollectionSelector('settings.downloadClients')
|
||||
);
|
||||
@@ -114,6 +120,13 @@ function ManageDownloadClientsModalContent(
|
||||
|
||||
const selectedCount = selectedIds.length;
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(value: string) => {
|
||||
dispatch(setManageDownloadClientsSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
}, [setIsDeleteModalOpen]);
|
||||
@@ -219,6 +232,9 @@ function ManageDownloadClientsModalContent(
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
|
||||
@@ -14,9 +14,11 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import useSelectState from 'Helpers/Hooks/useSelectState';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import {
|
||||
bulkDeleteIndexers,
|
||||
bulkEditIndexers,
|
||||
setManageIndexersSort,
|
||||
} from 'Store/Actions/settingsActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import { SelectStateInputProps } from 'typings/props';
|
||||
@@ -80,6 +82,8 @@ const COLUMNS = [
|
||||
|
||||
interface ManageIndexersModalContentProps {
|
||||
onModalClose(): void;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
}
|
||||
|
||||
function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
@@ -92,6 +96,8 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
isSaving,
|
||||
error,
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
}: IndexerAppState = useSelector(
|
||||
createClientSideCollectionSelector('settings.indexers')
|
||||
);
|
||||
@@ -112,6 +118,13 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
|
||||
const selectedCount = selectedIds.length;
|
||||
|
||||
const onSortPress = useCallback(
|
||||
(value: string) => {
|
||||
dispatch(setManageIndexersSort({ sortKey: value }));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
const onDeletePress = useCallback(() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
}, [setIsDeleteModalOpen]);
|
||||
@@ -214,6 +227,9 @@ function ManageIndexersModalContent(props: ManageIndexersModalContentProps) {
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onSortPress={onSortPress}
|
||||
>
|
||||
<TableBody>
|
||||
{items.map((item) => {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddRootFolder.css';
|
||||
|
||||
class AddRootFolder extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isAddNewRootFolderModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
onAddNewRootFolderPress = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: true });
|
||||
};
|
||||
|
||||
onNewRootFolderSelect = ({ value }) => {
|
||||
this.props.onNewRootFolderSelect(value);
|
||||
};
|
||||
|
||||
onAddRootFolderModalClose = () => {
|
||||
this.setState({ isAddNewRootFolderModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.addRootFolderButtonContainer}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={this.onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.importButtonIcon}
|
||||
name={icons.DRIVE}
|
||||
/>
|
||||
{translate('AddRootFolder')}
|
||||
</Button>
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={this.state.isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
value=""
|
||||
onChange={this.onNewRootFolderSelect}
|
||||
onModalClose={this.onAddRootFolderModalClose}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AddRootFolder.propTypes = {
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AddRootFolder;
|
||||
@@ -1,14 +1,18 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import Icon from 'Components/Icon';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||
import { addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import createRootFoldersSelector from 'Store/Selectors/createRootFoldersSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AddRootFolder.css';
|
||||
|
||||
function AddRootFolder() {
|
||||
const { isSaving, saveError } = useSelector(createRootFoldersSelector());
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [isAddNewRootFolderModalOpen, setIsAddNewRootFolderModalOpen] =
|
||||
@@ -30,24 +34,42 @@ function AddRootFolder() {
|
||||
}, [setIsAddNewRootFolderModalOpen]);
|
||||
|
||||
return (
|
||||
<div className={styles.addRootFolderButtonContainer}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
|
||||
{translate('AddRootFolder')}
|
||||
</Button>
|
||||
<>
|
||||
{!isSaving && saveError ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('AddRootFolderError')}
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
value=""
|
||||
onChange={onNewRootFolderSelect}
|
||||
onModalClose={onAddRootFolderModalClose}
|
||||
/>
|
||||
</div>
|
||||
<ul>
|
||||
{Array.isArray(saveError.responseJSON) ? (
|
||||
saveError.responseJSON.map((e, index) => {
|
||||
return <li key={index}>{e.errorMessage}</li>;
|
||||
})
|
||||
) : (
|
||||
<li>{JSON.stringify(saveError.responseJSON)}</li>
|
||||
)}
|
||||
</ul>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<div className={styles.addRootFolderButtonContainer}>
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
size={sizes.LARGE}
|
||||
onPress={onAddNewRootFolderPress}
|
||||
>
|
||||
<Icon className={styles.importButtonIcon} name={icons.DRIVE} />
|
||||
{translate('AddRootFolder')}
|
||||
</Button>
|
||||
|
||||
<FileBrowserModal
|
||||
isOpen={isAddNewRootFolderModalOpen}
|
||||
name="rootFolderPath"
|
||||
value=""
|
||||
onChange={onNewRootFolderSelect}
|
||||
onModalClose={onAddRootFolderModalClose}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
@@ -31,9 +33,9 @@ export const DELETE_DOWNLOAD_CLIENT = 'settings/downloadClients/deleteDownloadCl
|
||||
export const TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/testDownloadClient';
|
||||
export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestDownloadClient';
|
||||
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
||||
|
||||
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
||||
export const BULK_EDIT_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkEditDownloadClients';
|
||||
export const BULK_DELETE_DOWNLOAD_CLIENTS = 'settings/downloadClients/bulkDeleteDownloadClients';
|
||||
export const SET_MANAGE_DOWNLOAD_CLIENTS_SORT = 'settings/downloadClients/setManageDownloadClientsSort';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
@@ -48,9 +50,9 @@ export const deleteDownloadClient = createThunk(DELETE_DOWNLOAD_CLIENT);
|
||||
export const testDownloadClient = createThunk(TEST_DOWNLOAD_CLIENT);
|
||||
export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT);
|
||||
export const testAllDownloadClients = createThunk(TEST_ALL_DOWNLOAD_CLIENTS);
|
||||
|
||||
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
||||
export const bulkEditDownloadClients = createThunk(BULK_EDIT_DOWNLOAD_CLIENTS);
|
||||
export const bulkDeleteDownloadClients = createThunk(BULK_DELETE_DOWNLOAD_CLIENTS);
|
||||
export const setManageDownloadClientsSort = createAction(SET_MANAGE_DOWNLOAD_CLIENTS_SORT);
|
||||
|
||||
export const setDownloadClientValue = createAction(SET_DOWNLOAD_CLIENT_VALUE, (payload) => {
|
||||
return {
|
||||
@@ -90,7 +92,14 @@ export default {
|
||||
isTesting: false,
|
||||
isTestingAll: false,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
@@ -124,7 +133,10 @@ export default {
|
||||
|
||||
return selectedSchema;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
[SET_MANAGE_DOWNLOAD_CLIENTS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createAction } from 'redux-actions';
|
||||
import { sortDirections } from 'Helpers/Props';
|
||||
import createBulkEditItemHandler from 'Store/Actions/Creators/createBulkEditItemHandler';
|
||||
import createBulkRemoveItemHandler from 'Store/Actions/Creators/createBulkRemoveItemHandler';
|
||||
import createFetchHandler from 'Store/Actions/Creators/createFetchHandler';
|
||||
@@ -7,6 +8,7 @@ import createRemoveItemHandler from 'Store/Actions/Creators/createRemoveItemHand
|
||||
import createSaveProviderHandler, { createCancelSaveProviderHandler } from 'Store/Actions/Creators/createSaveProviderHandler';
|
||||
import createTestAllProvidersHandler from 'Store/Actions/Creators/createTestAllProvidersHandler';
|
||||
import createTestProviderHandler, { createCancelTestProviderHandler } from 'Store/Actions/Creators/createTestProviderHandler';
|
||||
import createSetClientSideCollectionSortReducer from 'Store/Actions/Creators/Reducers/createSetClientSideCollectionSortReducer';
|
||||
import createSetProviderFieldValueReducer from 'Store/Actions/Creators/Reducers/createSetProviderFieldValueReducer';
|
||||
import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/createSetSettingValueReducer';
|
||||
import { createThunk } from 'Store/thunks';
|
||||
@@ -35,9 +37,9 @@ export const DELETE_INDEXER = 'settings/indexers/deleteIndexer';
|
||||
export const TEST_INDEXER = 'settings/indexers/testIndexer';
|
||||
export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
||||
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
||||
|
||||
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
||||
export const BULK_EDIT_INDEXERS = 'settings/indexers/bulkEditIndexers';
|
||||
export const BULK_DELETE_INDEXERS = 'settings/indexers/bulkDeleteIndexers';
|
||||
export const SET_MANAGE_INDEXERS_SORT = 'settings/indexers/setManageIndexersSort';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
@@ -53,9 +55,9 @@ export const deleteIndexer = createThunk(DELETE_INDEXER);
|
||||
export const testIndexer = createThunk(TEST_INDEXER);
|
||||
export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
||||
export const testAllIndexers = createThunk(TEST_ALL_INDEXERS);
|
||||
|
||||
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
||||
export const bulkEditIndexers = createThunk(BULK_EDIT_INDEXERS);
|
||||
export const bulkDeleteIndexers = createThunk(BULK_DELETE_INDEXERS);
|
||||
export const setManageIndexersSort = createAction(SET_MANAGE_INDEXERS_SORT);
|
||||
|
||||
export const setIndexerValue = createAction(SET_INDEXER_VALUE, (payload) => {
|
||||
return {
|
||||
@@ -95,7 +97,14 @@ export default {
|
||||
isTesting: false,
|
||||
isTestingAll: false,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
@@ -146,7 +155,13 @@ export default {
|
||||
delete selectedSchema.name;
|
||||
|
||||
selectedSchema.fields = selectedSchema.fields.map((field) => {
|
||||
return { ...field };
|
||||
const newField = { ...field };
|
||||
|
||||
if (newField.privacy === 'apiKey' || newField.privacy === 'password') {
|
||||
newField.value = '';
|
||||
}
|
||||
|
||||
return newField;
|
||||
});
|
||||
|
||||
newState.selectedSchema = selectedSchema;
|
||||
@@ -157,7 +172,10 @@ export default {
|
||||
};
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
},
|
||||
|
||||
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -130,7 +130,10 @@ export const actionHandlers = handleThunks({
|
||||
|
||||
promise.done((data) => {
|
||||
const updatedItem = _.cloneDeep(data);
|
||||
updatedItem.internalId = updatedItem.id;
|
||||
updatedItem.id = updatedItem.tmdbId;
|
||||
delete updatedItem.images;
|
||||
|
||||
const actions = [
|
||||
updateItem({ section: 'movies', ...data }),
|
||||
updateItem({ section: 'addMovie', ...updatedItem }),
|
||||
|
||||
@@ -128,6 +128,22 @@ export const filterPredicates = {
|
||||
return predicate(originalLanguage ? originalLanguage.name : '', filterValue);
|
||||
},
|
||||
|
||||
releaseGroups: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const { statistics = {} } = item;
|
||||
const { releaseGroups = [] } = statistics;
|
||||
|
||||
return predicate(releaseGroups, filterValue);
|
||||
},
|
||||
|
||||
sizeOnDisk: function(item, filterValue, type) {
|
||||
const predicate = filterTypePredicates[type];
|
||||
const { statistics = {} } = item;
|
||||
const sizeOnDisk = statistics && statistics.sizeOnDisk ? statistics.sizeOnDisk : 0;
|
||||
|
||||
return predicate(sizeOnDisk, filterValue);
|
||||
},
|
||||
|
||||
inCinemas: function(item, filterValue, type) {
|
||||
return dateFilterPredicate(item.inCinemas, filterValue, type);
|
||||
},
|
||||
@@ -290,6 +306,12 @@ export const sortPredicates = {
|
||||
}
|
||||
|
||||
return Number.MAX_VALUE;
|
||||
},
|
||||
|
||||
sizeOnDisk: function(item) {
|
||||
const { statistics = {} } = item;
|
||||
|
||||
return statistics.sizeOnDisk || 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -98,6 +98,11 @@ export const defaultState = {
|
||||
}),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'dateAdded',
|
||||
label: () => translate('Added'),
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
columnLabel: () => translate('Actions'),
|
||||
|
||||
@@ -206,10 +206,16 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'releaseGroups',
|
||||
label: () => translate('ReleaseGroup'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: () => translate('Tags'),
|
||||
isSortable: false,
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
@@ -241,6 +247,17 @@ export const defaultState = {
|
||||
return originalLanguage.name;
|
||||
},
|
||||
|
||||
releaseGroups: function(item) {
|
||||
const { statistics = {} } = item;
|
||||
const { releaseGroups = [] } = statistics;
|
||||
|
||||
return releaseGroups.length ?
|
||||
releaseGroups
|
||||
.map((group) => group.toLowerCase())
|
||||
.sort((a, b) => a.localeCompare(b)) :
|
||||
undefined;
|
||||
},
|
||||
|
||||
imdbRating: function(item) {
|
||||
const { ratings = {} } = item;
|
||||
|
||||
@@ -313,6 +330,28 @@ export const defaultState = {
|
||||
return collectionList.sort(sortByName);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'releaseGroups',
|
||||
label: () => translate('ReleaseGroups'),
|
||||
type: filterBuilderTypes.ARRAY,
|
||||
optionsSelector: function(items) {
|
||||
const groupList = items.reduce((acc, movie) => {
|
||||
const { statistics = {} } = movie;
|
||||
const { releaseGroups = [] } = statistics;
|
||||
|
||||
releaseGroups.forEach((releaseGroup) => {
|
||||
acc.push({
|
||||
id: releaseGroup,
|
||||
name: releaseGroup
|
||||
});
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return groupList.sort(sortByName);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'status',
|
||||
label: () => translate('ReleaseStatus'),
|
||||
|
||||
@@ -147,6 +147,12 @@ export const defaultState = {
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'added',
|
||||
label: () => translate('Added'),
|
||||
isSortable: true,
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'progress',
|
||||
label: () => translate('Progress'),
|
||||
@@ -413,13 +419,14 @@ export const actionHandlers = handleThunks({
|
||||
id,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(updateItem({ section: paged, id, isRemoving: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
||||
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||
method: 'DELETE'
|
||||
}).request;
|
||||
|
||||
@@ -437,7 +444,8 @@ export const actionHandlers = handleThunks({
|
||||
ids,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(batchActions([
|
||||
@@ -453,7 +461,7 @@ export const actionHandlers = handleThunks({
|
||||
]));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
|
||||
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
function createDimensionsSelector() {
|
||||
return createSelector(
|
||||
(state) => state.app.dimensions,
|
||||
(state: AppState) => state.app.dimensions,
|
||||
(dimensions) => {
|
||||
return dimensions;
|
||||
}
|
||||
@@ -215,6 +215,8 @@ module.exports = {
|
||||
calendarTextDim: '#eee',
|
||||
calendarTextDimAlternate: '#fff',
|
||||
|
||||
calendarFullColorFilter: 'grayscale(90%) contrast(200%) saturate(50%)',
|
||||
|
||||
//
|
||||
// Table
|
||||
|
||||
|
||||
@@ -216,6 +216,8 @@ module.exports = {
|
||||
calendarTextDim: '#666',
|
||||
calendarTextDimAlternate: '#242424',
|
||||
|
||||
calendarFullColorFilter: 'brightness(30%)',
|
||||
|
||||
//
|
||||
// Table
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ class About extends Component {
|
||||
isNetCore,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -73,13 +73,13 @@ class About extends Component {
|
||||
}
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('DBMigration')}
|
||||
data={migrationVersion}
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title={translate('Database')}
|
||||
data={`${titleCase(databaseType)} ${databaseVersion}`}
|
||||
title={translate('DatabaseMigration')}
|
||||
data={migrationVersion}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
@@ -121,9 +121,9 @@ About.propTypes = {
|
||||
isNetCore: PropTypes.bool.isRequired,
|
||||
runtimeVersion: PropTypes.string.isRequired,
|
||||
isDocker: PropTypes.bool.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
databaseType: PropTypes.string.isRequired,
|
||||
databaseVersion: PropTypes.string.isRequired,
|
||||
migrationVersion: PropTypes.number.isRequired,
|
||||
appData: PropTypes.string.isRequired,
|
||||
startupPath: PropTypes.string.isRequired,
|
||||
mode: PropTypes.string.isRequired,
|
||||
|
||||
@@ -28,6 +28,7 @@ interface Queue extends ModelBase {
|
||||
sizeleft: number;
|
||||
timeleft: string;
|
||||
estimatedCompletionTime: string;
|
||||
added?: string;
|
||||
status: string;
|
||||
trackedDownloadStatus: QueueTrackedDownloadStatus;
|
||||
trackedDownloadState: QueueTrackedDownloadState;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface UiSettings {
|
||||
theme: string;
|
||||
showRelativeDates: boolean;
|
||||
shortDateFormat: string;
|
||||
longDateFormat: string;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@juggle/resize-observer": "3.4.0",
|
||||
"@microsoft/signalr": "6.0.21",
|
||||
"@microsoft/signalr": "6.0.25",
|
||||
"@sentry/browser": "7.51.2",
|
||||
"@sentry/integrations": "7.51.2",
|
||||
"@types/node": "18.16.8",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Test.Common;
|
||||
using Radarr.Http.ClientSchema;
|
||||
|
||||
@@ -9,6 +12,16 @@ namespace NzbDrone.Api.Test.ClientSchemaTests
|
||||
[TestFixture]
|
||||
public class SchemaBuilderFixture : TestBase
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.GetMock<ILocalizationService>()
|
||||
.Setup(s => s.GetLocalizedString(It.IsAny<string>(), It.IsAny<Dictionary<string, object>>()))
|
||||
.Returns<string, Dictionary<string, object>>((s, d) => s);
|
||||
|
||||
SchemaBuilder.Initialize(Mocker.Container);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_field_for_every_property()
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@ namespace NzbDrone.Common.Test.Http
|
||||
// Use mirrors for tests that use two hosts
|
||||
var candidates = new[] { "httpbin1.servarr.com" };
|
||||
|
||||
// httpbin.org is broken right now, occassionally redirecting to https if it's unavailable.
|
||||
// httpbin.org is broken right now, occasionally redirecting to https if it's unavailable.
|
||||
_httpBinHost = mainHost;
|
||||
_httpBinHosts = candidates.Where(IsTestSiteAvailable).ToArray();
|
||||
|
||||
@@ -129,6 +129,16 @@ namespace NzbDrone.Common.Test.Http
|
||||
response.Content.Should().NotBeNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_timeout_request()
|
||||
{
|
||||
var request = new HttpRequest($"https://{_httpBinHost}/delay/10");
|
||||
|
||||
request.RequestTimeout = new TimeSpan(0, 0, 5);
|
||||
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.ExecuteAsync(request));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task should_execute_https_get()
|
||||
{
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace NzbDrone.Common.Disk
|
||||
try
|
||||
{
|
||||
var testPath = Path.Combine(path, "radarr_write_test.txt");
|
||||
var testContent = string.Format("This file was created to verify if '{0}' is writable. It should've been automatically deleted. Feel free to delete it.", path);
|
||||
var testContent = $"This file was created to verify if '{path}' is writable. It should've been automatically deleted. Feel free to delete it.";
|
||||
WriteAllText(testPath, testContent);
|
||||
File.Delete(testPath);
|
||||
return true;
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace NzbDrone.Common.EnvironmentInfo
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Coudn't set app folder permission");
|
||||
_logger.Warn(ex, "Couldn't set app folder permission");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -102,31 +102,38 @@ namespace NzbDrone.Common.Http.Dispatchers
|
||||
|
||||
var httpClient = GetClient(request.Url);
|
||||
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
try
|
||||
{
|
||||
byte[] data = null;
|
||||
|
||||
try
|
||||
using var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cts.Token);
|
||||
{
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
byte[] data = null;
|
||||
|
||||
try
|
||||
{
|
||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||
if (request.ResponseStream != null && responseMessage.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
await responseMessage.Content.CopyToAsync(request.ResponseStream, null, cts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||
}
|
||||
}
|
||||
else
|
||||
catch (Exception ex)
|
||||
{
|
||||
data = await responseMessage.Content.ReadAsByteArrayAsync(cts.Token);
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||
}
|
||||
|
||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||
|
||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new WebException("Failed to read complete http response", ex, WebExceptionStatus.ReceiveFailure, null);
|
||||
}
|
||||
|
||||
var headers = responseMessage.Headers.ToNameValueCollection();
|
||||
|
||||
headers.Add(responseMessage.Content.Headers.ToNameValueCollection());
|
||||
|
||||
return new HttpResponse(request, new HttpHeader(headers), data, responseMessage.StatusCode, responseMessage.Version);
|
||||
}
|
||||
catch (OperationCanceledException ex) when (cts.IsCancellationRequested)
|
||||
{
|
||||
throw new WebException("Http request timed out", ex.InnerException, WebExceptionStatus.Timeout, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,10 @@ namespace NzbDrone.Common.Http
|
||||
{
|
||||
data = new XElement("base64", Convert.ToBase64String(bytes));
|
||||
}
|
||||
else if (value is Dictionary<string, string> d)
|
||||
{
|
||||
data = new XElement("struct", d.Select(p => new XElement("member", new XElement("name", p.Key), new XElement("value", p.Value))));
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidOperationException($"Unhandled argument type {value.GetType().Name}");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user