mirror of
https://github.com/Readarr/Readarr.git
synced 2026-03-30 18:24:45 -04:00
Compare commits
107 Commits
v0.3.13.23
...
v0.3.19.24
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c77d820763 | ||
|
|
3327ed0f49 | ||
|
|
44009e980b | ||
|
|
02fd733223 | ||
|
|
2fa9576d05 | ||
|
|
c7ee278ee4 | ||
|
|
d72c27ceed | ||
|
|
7a20fe2288 | ||
|
|
042b62a2a5 | ||
|
|
88141e9d63 | ||
|
|
7fa1114edf | ||
|
|
d4262532e2 | ||
|
|
a21f83aae1 | ||
|
|
d659e86a7d | ||
|
|
0b924005ec | ||
|
|
ba2fad5d9c | ||
|
|
58416cee67 | ||
|
|
38124313c7 | ||
|
|
3fc9f6c0a4 | ||
|
|
79ce5abd53 | ||
|
|
7f01d597cb | ||
|
|
31f35df71d | ||
|
|
faeb78801c | ||
|
|
bd5695f2dd | ||
|
|
5375cbe1c2 | ||
|
|
d0b797ea61 | ||
|
|
e76f160695 | ||
|
|
ef71fc1b41 | ||
|
|
14f14e5da4 | ||
|
|
bd265e47fa | ||
|
|
333d344c0b | ||
|
|
db6712f030 | ||
|
|
1065a6283c | ||
|
|
1b40c5c7ce | ||
|
|
a8de87300e | ||
|
|
f260078ac8 | ||
|
|
5a6486be21 | ||
|
|
2e9de3cb86 | ||
|
|
a259684916 | ||
|
|
5704adfbc5 | ||
|
|
6cfaab07ba | ||
|
|
b36085a3cc | ||
|
|
0afa0977b0 | ||
|
|
4a174e559f | ||
|
|
0fb8ab2280 | ||
|
|
261b0f398b | ||
|
|
d1fea384a7 | ||
|
|
9542ea0d2e | ||
|
|
e1d697c561 | ||
|
|
22ed847849 | ||
|
|
2faef704b4 | ||
|
|
a566c3e21f | ||
|
|
cc0d2a84ae | ||
|
|
1c3d2ce4e5 | ||
|
|
57f614f4cd | ||
|
|
9d2efe0944 | ||
|
|
e032be48e0 | ||
|
|
cd66de1992 | ||
|
|
3066dd92d7 | ||
|
|
467a87baec | ||
|
|
80fb077c94 | ||
|
|
07433d69ca | ||
|
|
3b3ebe463c | ||
|
|
03392ca635 | ||
|
|
d23ce9ecc2 | ||
|
|
e968fcaff6 | ||
|
|
31da559f89 | ||
|
|
a093290792 | ||
|
|
9e3dfc510d | ||
|
|
9d27c172ac | ||
|
|
518dbe53eb | ||
|
|
f9ba00c9e7 | ||
|
|
4aec7a0ea7 | ||
|
|
fc4cf8e81e | ||
|
|
143de3b220 | ||
|
|
e1a07d01b2 | ||
|
|
27e498bb14 | ||
|
|
b9f1882a57 | ||
|
|
2392573c39 | ||
|
|
2351efd013 | ||
|
|
526429bde4 | ||
|
|
abd44b59bc | ||
|
|
9942457ffc | ||
|
|
073342ef39 | ||
|
|
b455708f2e | ||
|
|
622b02c478 | ||
|
|
8effba383d | ||
|
|
2749479283 | ||
|
|
4cbafa76d8 | ||
|
|
73782cc233 | ||
|
|
de396fe9be | ||
|
|
71cb9e1dd7 | ||
|
|
ee5ed57fcc | ||
|
|
d20a049a5a | ||
|
|
a9f77ace37 | ||
|
|
0341a2ec26 | ||
|
|
d6796bbe1a | ||
|
|
9066f8558c | ||
|
|
c4e37528ee | ||
|
|
5937c952af | ||
|
|
0f4bd3c472 | ||
|
|
cf415e61de | ||
|
|
9865e92cea | ||
|
|
1cf956a9d9 | ||
|
|
8989c55c8c | ||
|
|
dc83e0127e | ||
|
|
34eb312426 |
16
.github/label-actions.yml
vendored
Normal file
16
.github/label-actions.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Configuration for Label Actions - https://github.com/dessant/label-actions
|
||||
|
||||
'Type: Support':
|
||||
comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
|
||||
close: true
|
||||
close-reason: 'not planned'
|
||||
|
||||
'Status: Logs Needed':
|
||||
comment: >
|
||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
|
||||
17
.github/workflows/label-actions.yml
vendored
Normal file
17
.github/workflows/label-actions.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: 'Label Actions'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v3
|
||||
with:
|
||||
process-only: 'issues'
|
||||
31
.github/workflows/support.yml
vendored
31
.github/workflows/support.yml
vendored
@@ -1,31 +0,0 @@
|
||||
name: 'Support requests'
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled, unlabeled, reopened]
|
||||
|
||||
jobs:
|
||||
support:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/support-requests@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Type: Support'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, we use the issue tracker exclusively
|
||||
for bug reports and feature requests. However, this issue appears
|
||||
to be a support request. Please hop over onto our [Discord](https://readarr.com/discord).
|
||||
close-issue: true
|
||||
lock-issue: false
|
||||
- uses: dessant/support-requests@v3
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
support-label: 'Status: Logs Needed'
|
||||
issue-comment: >
|
||||
:wave: @{issue-author}, In order to help you further we'll need to see logs.
|
||||
You'll need to enable trace logging and replicate the problem that you encountered.
|
||||
Guidance on how to enable trace logging can be found in
|
||||
our [troubleshooting guide](https://wiki.servarr.com/readarr/troubleshooting#logging-and-log-files).
|
||||
close-issue: false
|
||||
lock-issue: false
|
||||
@@ -9,7 +9,7 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '0.3.13'
|
||||
majorVersion: '0.3.19'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
readarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(readarrVersion)'
|
||||
|
||||
@@ -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 }],
|
||||
|
||||
@@ -23,7 +23,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import RemoveQueueItemModal from './RemoveQueueItemModal';
|
||||
|
||||
class Queue extends Component {
|
||||
|
||||
@@ -289,9 +289,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);
|
||||
@@ -299,7 +306,7 @@ class Queue extends Component {
|
||||
return !!(item && item.authorId && item.bookId);
|
||||
})
|
||||
)}
|
||||
allPending={isConfirmRemoveModalOpen && (
|
||||
pending={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ class QueueRow extends Component {
|
||||
indexer,
|
||||
outputPath,
|
||||
downloadClient,
|
||||
downloadClientHasPostImportCategory,
|
||||
downloadForced,
|
||||
estimatedCompletionTime,
|
||||
timeleft,
|
||||
@@ -389,6 +390,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canChangeCategory={!!downloadClientHasPostImportCategory}
|
||||
canIgnore={!!author}
|
||||
isPending={isPending}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
@@ -418,6 +420,7 @@ QueueRow.propTypes = {
|
||||
indexer: PropTypes.string,
|
||||
outputPath: PropTypes.string,
|
||||
downloadClient: PropTypes.string,
|
||||
downloadClientHasPostImportCategory: PropTypes.bool,
|
||||
downloadForced: PropTypes.bool.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
timeleft: PropTypes.string,
|
||||
|
||||
@@ -1,177 +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>
|
||||
Remove - {sourceTitle}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
{
|
||||
isPending ?
|
||||
null :
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('RemoveFromDownloadClient')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning={translate('RemoveHelpTextWarning')}
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
{translate('BlocklistRelease')}
|
||||
</FormLabel>
|
||||
|
||||
<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>
|
||||
}
|
||||
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={this.onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
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,178 +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>
|
||||
}
|
||||
|
||||
</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;
|
||||
@@ -44,6 +44,10 @@
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.filterIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.authorNavigationButtons {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
||||
@@ -6,6 +6,7 @@ interface CssExports {
|
||||
'authorUpButton': string;
|
||||
'contentContainer': string;
|
||||
'errorMessage': string;
|
||||
'filterIcon': string;
|
||||
'innerContentBody': string;
|
||||
'metadataMessage': string;
|
||||
'selectedTab': string;
|
||||
|
||||
@@ -239,9 +239,14 @@ class AuthorDetails extends Component {
|
||||
saveError,
|
||||
isDeleting,
|
||||
deleteError,
|
||||
statistics
|
||||
statistics = {}
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
@@ -435,7 +440,7 @@ class AuthorDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('BooksTotal', [statistics.totalBookCount])}
|
||||
{translate('BooksTotal', [totalBookCount])}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
@@ -463,7 +468,7 @@ class AuthorDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
{translate('FilesTotal', [statistics.bookFileCount])}
|
||||
{translate('FilesTotal', [bookFileCount])}
|
||||
</Tab>
|
||||
|
||||
{
|
||||
|
||||
@@ -155,7 +155,6 @@ function createMapStateToProps() {
|
||||
const isRefreshing = isAuthorRefreshing || allAuthorRefreshing;
|
||||
const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.AUTHOR_SEARCH, authorId: author.id }));
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||
|
||||
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||
const isRenamingAuthor = (
|
||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthorHistoryContentConnector from './AuthorHistoryContentConnector';
|
||||
import AuthorHistoryModalContent from './AuthorHistoryModalContent';
|
||||
|
||||
@@ -14,6 +15,7 @@ function AuthorHistoryModal(props) {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthorHistoryContentConnector
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 translate from 'Utilities/String/translate';
|
||||
import AuthorHistoryTableContent from './AuthorHistoryTableContent';
|
||||
|
||||
class AuthorHistoryModalContent extends Component {
|
||||
@@ -20,7 +21,7 @@ class AuthorHistoryModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
History
|
||||
{translate('History')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
@@ -31,7 +32,7 @@ class AuthorHistoryModalContent extends Component {
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
{translate('Close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.details,
|
||||
.actions {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'details': string;
|
||||
'sourceTitle': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import BookFormats from 'Book/BookFormats';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
@@ -11,6 +12,7 @@ import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './AuthorHistoryRow.css';
|
||||
|
||||
@@ -75,6 +77,8 @@ class AuthorHistoryRow extends Component {
|
||||
sourceTitle,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
date,
|
||||
data,
|
||||
book
|
||||
@@ -106,11 +110,19 @@ class AuthorHistoryRow extends Component {
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<BookFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{formatCustomFormatScore(customFormatScore, customFormats.length)}
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={date}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.details}>
|
||||
<TableRowCell className={styles.actions}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
@@ -127,14 +139,13 @@ class AuthorHistoryRow extends Component {
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
title={translate('MarkAsFailed')}
|
||||
name={icons.REMOVE}
|
||||
size={14}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
@@ -160,6 +171,8 @@ AuthorHistoryRow.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
fullAuthor: PropTypes.bool.isRequired,
|
||||
|
||||
9
frontend/src/Author/History/AuthorHistoryTable.css
Normal file
9
frontend/src/Author/History/AuthorHistoryTable.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
7
frontend/src/Author/History/AuthorHistoryTable.css.d.ts
vendored
Normal file
7
frontend/src/Author/History/AuthorHistoryTable.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'container': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import AuthorHistoryContentConnector from 'Author/History/AuthorHistoryContentConnector';
|
||||
import AuthorHistoryTableContent from 'Author/History/AuthorHistoryTableContent';
|
||||
import styles from './AuthorHistoryTable.css';
|
||||
|
||||
function AuthorHistoryTable(props) {
|
||||
const {
|
||||
@@ -8,10 +9,12 @@ function AuthorHistoryTable(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<AuthorHistoryContentConnector
|
||||
component={AuthorHistoryTableContent}
|
||||
{...otherProps}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<AuthorHistoryContentConnector
|
||||
component={AuthorHistoryTableContent}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.blankpad {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
7
frontend/src/Author/History/AuthorHistoryTableContent.css.d.ts
vendored
Normal file
7
frontend/src/Author/History/AuthorHistoryTableContent.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'blankpad': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,12 +1,14 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AuthorHistoryRowConnector from './AuthorHistoryRowConnector';
|
||||
import styles from './AuthorHistoryTableContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
@@ -15,32 +17,41 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'book',
|
||||
label: 'Book',
|
||||
label: () => translate('Book'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: 'Source Title',
|
||||
label: () => translate( 'SourceTitle'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
label: () => translate('Quality'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormats',
|
||||
label: () => translate('CustomFormats'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: 'Date',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
label: 'Details',
|
||||
label: () => translate('Date'),
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: 'Actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
@@ -64,7 +75,7 @@ class AuthorHistoryTableContent extends Component {
|
||||
const hasItems = !!items.length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
@@ -79,7 +90,7 @@ class AuthorHistoryTableContent extends Component {
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div>
|
||||
<div className={styles.blankpad}>
|
||||
{translate('NoHistory')}
|
||||
</div>
|
||||
}
|
||||
@@ -103,7 +114,7 @@ class AuthorHistoryTableContent extends Component {
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ class AuthorIndexOverview extends Component {
|
||||
status,
|
||||
titleSlug,
|
||||
nextAiring,
|
||||
statistics,
|
||||
statistics = {},
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
@@ -113,10 +113,11 @@ class AuthorIndexOverview extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
sizeOnDisk,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -179,6 +180,7 @@ class AuthorIndexOverview extends Component {
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
bookCount={bookCount}
|
||||
availableBookCount={availableBookCount}
|
||||
bookFileCount={bookFileCount}
|
||||
totalBookCount={totalBookCount}
|
||||
posterWidth={posterWidth}
|
||||
|
||||
@@ -85,7 +85,7 @@ class AuthorIndexPoster extends Component {
|
||||
titleSlug,
|
||||
status,
|
||||
nextAiring,
|
||||
statistics,
|
||||
statistics = {},
|
||||
images,
|
||||
posterWidth,
|
||||
posterHeight,
|
||||
@@ -110,10 +110,11 @@ class AuthorIndexPoster extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
sizeOnDisk,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -213,6 +214,7 @@ class AuthorIndexPoster extends Component {
|
||||
monitored={monitored}
|
||||
status={status}
|
||||
bookCount={bookCount}
|
||||
availableBookCount={availableBookCount}
|
||||
bookFileCount={bookFileCount}
|
||||
totalBookCount={totalBookCount}
|
||||
posterWidth={posterWidth}
|
||||
|
||||
@@ -11,14 +11,15 @@ function AuthorIndexProgressBar(props) {
|
||||
monitored,
|
||||
status,
|
||||
bookCount,
|
||||
availableBookCount,
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
posterWidth,
|
||||
detailedProgressBar
|
||||
} = props;
|
||||
|
||||
const progress = bookCount ? bookCount / totalBookCount * 100 : 100;
|
||||
const text = `${bookCount} / ${totalBookCount}`;
|
||||
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||
const text = `${availableBookCount} / ${bookCount}`;
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
@@ -29,7 +30,7 @@ function AuthorIndexProgressBar(props) {
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
showText={detailedProgressBar}
|
||||
text={text}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||
width={posterWidth}
|
||||
/>
|
||||
);
|
||||
@@ -39,6 +40,7 @@ AuthorIndexProgressBar.propTypes = {
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
bookCount: PropTypes.number.isRequired,
|
||||
availableBookCount: PropTypes.number.isRequired,
|
||||
bookFileCount: PropTypes.number.isRequired,
|
||||
totalBookCount: PropTypes.number.isRequired,
|
||||
posterWidth: PropTypes.number.isRequired,
|
||||
|
||||
@@ -90,7 +90,7 @@ class AuthorIndexRow extends Component {
|
||||
nextBook,
|
||||
lastBook,
|
||||
added,
|
||||
statistics,
|
||||
statistics = {},
|
||||
genres,
|
||||
ratings,
|
||||
path,
|
||||
@@ -110,10 +110,11 @@ class AuthorIndexRow extends Component {
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookCount,
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
sizeOnDisk
|
||||
bookCount = 0,
|
||||
availableBookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
sizeOnDisk = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
@@ -286,7 +287,7 @@ class AuthorIndexRow extends Component {
|
||||
}
|
||||
|
||||
if (name === 'bookProgress') {
|
||||
const progress = bookCount ? bookFileCount / bookCount * 100 : 100;
|
||||
const progress = bookCount ? (availableBookCount / bookCount) * 100 : 100;
|
||||
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
@@ -297,8 +298,8 @@ class AuthorIndexRow extends Component {
|
||||
progress={progress}
|
||||
kind={getProgressBarKind(status, monitored, progress)}
|
||||
showText={true}
|
||||
text={`${bookCount} / ${totalBookCount}`}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
text={`${availableBookCount} / ${bookCount}`}
|
||||
title={translate('AuthorProgressBarText', { bookCount, availableBookCount, bookFileCount, totalBookCount })}
|
||||
width={125}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
|
||||
@@ -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 BookQuality(props) {
|
||||
const {
|
||||
className,
|
||||
@@ -35,7 +66,8 @@ function BookQuality(props) {
|
||||
quality,
|
||||
size,
|
||||
isMonitored,
|
||||
isCutoffNotMet
|
||||
isCutoffNotMet,
|
||||
showRevision
|
||||
} = props;
|
||||
|
||||
let kind = kinds.DEFAULT;
|
||||
@@ -50,13 +82,15 @@ function BookQuality(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 @@ BookQuality.propTypes = {
|
||||
quality: PropTypes.object.isRequired,
|
||||
size: PropTypes.number,
|
||||
isMonitored: PropTypes.bool,
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
isCutoffNotMet: PropTypes.bool,
|
||||
showRevision: PropTypes.bool
|
||||
};
|
||||
|
||||
BookQuality.defaultProps = {
|
||||
title: '',
|
||||
isMonitored: true
|
||||
isMonitored: true,
|
||||
showRevision: false
|
||||
};
|
||||
|
||||
export default BookQuality;
|
||||
|
||||
@@ -99,9 +99,14 @@ class BookDetails extends Component {
|
||||
nextBook,
|
||||
isSearching,
|
||||
onRefreshPress,
|
||||
onSearchPress
|
||||
onSearchPress,
|
||||
statistics = {}
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount = 0
|
||||
} = statistics;
|
||||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
@@ -238,21 +243,21 @@ class BookDetails extends Component {
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
History
|
||||
{translate('History')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
Search
|
||||
{translate('Search')}
|
||||
</Tab>
|
||||
|
||||
<Tab
|
||||
className={styles.tab}
|
||||
selectedClassName={styles.selectedTab}
|
||||
>
|
||||
Files
|
||||
{translate('FilesTotal', [bookFileCount])}
|
||||
</Tab>
|
||||
|
||||
{
|
||||
@@ -335,6 +340,7 @@ BookDetails.propTypes = {
|
||||
ratings: PropTypes.object.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
statistics: PropTypes.object.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
shortDateFormat: PropTypes.string.isRequired,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -69,16 +69,21 @@ function createMapStateToProps() {
|
||||
|
||||
const previousBook = sortedBooks[bookIndex - 1] || _.last(sortedBooks);
|
||||
const nextBook = sortedBooks[bookIndex + 1] || _.first(sortedBooks);
|
||||
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||
const isRefreshing = (
|
||||
isCommandExecuting(isRefreshingCommand) &&
|
||||
isRefreshingCommand.body.bookId === book.id
|
||||
);
|
||||
const isSearchingCommand = findCommand(commands, { name: commandNames.BOOK_SEARCH });
|
||||
const isSearching = (
|
||||
isCommandExecuting(isSearchingCommand) &&
|
||||
isSearchingCommand.body.bookIds.indexOf(book.id) > -1
|
||||
);
|
||||
|
||||
const isRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_BOOK });
|
||||
const isRefreshing = (
|
||||
isCommandExecuting(isRefreshingCommand) &&
|
||||
isRefreshingCommand.body.bookId === book.id
|
||||
const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, authorId: author.id }));
|
||||
const isRenamingAuthorCommand = findCommand(commands, { name: commandNames.RENAME_AUTHOR });
|
||||
const isRenamingAuthor = (
|
||||
isCommandExecuting(isRenamingAuthorCommand) &&
|
||||
isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1
|
||||
);
|
||||
|
||||
const isFetching = isBookFilesFetching || editions.isFetching;
|
||||
@@ -90,6 +95,8 @@ function createMapStateToProps() {
|
||||
author,
|
||||
isRefreshing,
|
||||
isSearching,
|
||||
isRenamingFiles,
|
||||
isRenamingAuthor,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
bookFilesError,
|
||||
@@ -125,9 +132,27 @@ class BookDetailsConnector extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.id !== this.props.id ||
|
||||
const {
|
||||
id,
|
||||
anyReleaseOk,
|
||||
isRenamingFiles,
|
||||
isRenamingAuthor
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
(prevProps.isRenamingFiles && !isRenamingFiles) ||
|
||||
(prevProps.isRenamingAuthor && !isRenamingAuthor) ||
|
||||
!_.isEqual(getMonitoredEditions(prevProps), getMonitoredEditions(this.props)) ||
|
||||
(prevProps.anyReleaseOk === false && this.props.anyReleaseOk === true)) {
|
||||
(prevProps.anyReleaseOk === false && anyReleaseOk === true)
|
||||
) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
|
||||
// If the id has changed we need to clear the book
|
||||
// files and fetch from the server.
|
||||
|
||||
if (prevProps.id !== id) {
|
||||
this.unpopulate();
|
||||
this.populate();
|
||||
}
|
||||
@@ -197,6 +222,8 @@ class BookDetailsConnector extends Component {
|
||||
BookDetailsConnector.propTypes = {
|
||||
id: PropTypes.number,
|
||||
anyReleaseOk: PropTypes.bool,
|
||||
isRenamingFiles: PropTypes.bool.isRequired,
|
||||
isRenamingAuthor: PropTypes.bool.isRequired,
|
||||
isBookFetching: PropTypes.bool,
|
||||
isBookPopulated: PropTypes.bool,
|
||||
titleSlug: PropTypes.string.isRequired,
|
||||
|
||||
@@ -16,8 +16,8 @@ function BookIndexProgressBar(props) {
|
||||
detailedProgressBar
|
||||
} = props;
|
||||
|
||||
const progress = bookCount ? bookFileCount / totalBookCount * 100 : 0;
|
||||
const text = `${bookFileCount} / ${bookCount}`;
|
||||
const progress = bookFileCount && bookCount ? (totalBookCount / bookCount) * 100 : 0;
|
||||
const text = `${bookFileCount ? bookCount : 0} / ${totalBookCount}`;
|
||||
|
||||
return (
|
||||
<ProgressBar
|
||||
@@ -28,7 +28,11 @@ function BookIndexProgressBar(props) {
|
||||
size={detailedProgressBar ? sizes.MEDIUM : sizes.SMALL}
|
||||
showText={detailedProgressBar}
|
||||
text={text}
|
||||
title={translate('BookFileCountBookCountTotalTotalBookCountInterp', [bookFileCount, bookCount, totalBookCount])}
|
||||
title={translate('BookProgressBarText', {
|
||||
bookCount: bookFileCount ? bookCount : 0,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
})}
|
||||
width={posterWidth}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -229,7 +229,6 @@ class BookIndexRow extends Component {
|
||||
className={styles[name]}
|
||||
>
|
||||
{bookFileCount}
|
||||
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
9
frontend/src/BookFile/Editor/BookFileEditorTable.css
Normal file
9
frontend/src/BookFile/Editor/BookFileEditorTable.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.container {
|
||||
border: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
background-color: var(--inputBackgroundColor);
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
7
frontend/src/BookFile/Editor/BookFileEditorTable.css.d.ts
vendored
Normal file
7
frontend/src/BookFile/Editor/BookFileEditorTable.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'container': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
|
||||
import styles from './BookFileEditorTable.css';
|
||||
|
||||
function BookFileEditorTable(props) {
|
||||
const {
|
||||
@@ -7,9 +8,11 @@ function BookFileEditorTable(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<BookFileEditorTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<BookFileEditorTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.filesTable {
|
||||
margin-bottom: 20px;
|
||||
padding-top: 15px;
|
||||
margin: 10px;
|
||||
padding-top: 5px;
|
||||
border: 1px solid var(--borderColor);
|
||||
border-top: 1px solid var(--borderColor);
|
||||
border-radius: 4px;
|
||||
@@ -13,9 +13,15 @@
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.selectInput {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.blankpad {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 2em;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'blankpad': string;
|
||||
'filesTable': string;
|
||||
'selectInput': string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
@@ -120,7 +121,7 @@ class BookFileEditorTableContent extends Component {
|
||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
{
|
||||
isFetching && !isPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
@@ -129,13 +130,13 @@ class BookFileEditorTableContent extends Component {
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<div>{error}</div> :
|
||||
<Alert kind={kinds.DANGER}>{error}</Alert> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length ?
|
||||
<div>
|
||||
<div className={styles.blankpad}>
|
||||
No book files to manage.
|
||||
</div> :
|
||||
null
|
||||
@@ -173,26 +174,30 @@ class BookFileEditorTableContent extends Component {
|
||||
null
|
||||
}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onPress={this.onDeletePress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton>
|
||||
{
|
||||
isPopulated && items.length ? (
|
||||
<div className={styles.actions}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onPress={this.onDeletePress}
|
||||
>
|
||||
{translate('Delete')}
|
||||
</SpinnerButton>
|
||||
|
||||
<div className={styles.selectInput}>
|
||||
<SelectInput
|
||||
name="quality"
|
||||
value="selectQuality"
|
||||
values={qualityOptions}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.selectInput}>
|
||||
<SelectInput
|
||||
name="quality"
|
||||
value="selectQuality"
|
||||
values={qualityOptions}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
@@ -203,7 +208,7 @@ class BookFileEditorTableContent extends Component {
|
||||
onConfirm={this.onConfirmDelete}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,14 +27,15 @@ class BookshelfBook extends Component {
|
||||
title,
|
||||
disambiguation,
|
||||
monitored,
|
||||
statistics,
|
||||
statistics = {},
|
||||
isSaving
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
bookFileCount,
|
||||
totalBookCount,
|
||||
percentOfBooks
|
||||
bookCount = 0,
|
||||
bookFileCount = 0,
|
||||
totalBookCount = 0,
|
||||
percentOfBooks = 0
|
||||
} = statistics;
|
||||
|
||||
return (
|
||||
@@ -59,10 +60,14 @@ class BookshelfBook extends Component {
|
||||
percentOfBooks < 100 && monitored && styles.missingWanted,
|
||||
percentOfBooks === 100 && styles.allBooks
|
||||
)}
|
||||
title={translate('BookFileCounttotalBookCountBooksDownloadedInterp', [bookFileCount, totalBookCount])}
|
||||
title={translate('BookProgressBarText', {
|
||||
bookCount: bookFileCount ? bookCount : 0,
|
||||
bookFileCount,
|
||||
totalBookCount
|
||||
})}
|
||||
>
|
||||
{
|
||||
totalBookCount === 0 ? '0/0' : `${bookFileCount}/${totalBookCount}`
|
||||
totalBookCount === 0 ? '0/0' : `${bookFileCount ? bookCount : 0}/${totalBookCount}`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,22 +29,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}>
|
||||
|
||||
@@ -25,7 +25,8 @@ function createMapStateToProps() {
|
||||
const values = _.map(filteredItems.sort(sortByName), (downloadClient) => {
|
||||
return {
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name
|
||||
value: downloadClient.name,
|
||||
hint: `(${downloadClient.id})`
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -273,6 +273,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,
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: $formLabelRightMarginWidth;
|
||||
padding-top: 8px;
|
||||
min-height: 35px;
|
||||
text-align: end;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
|
||||
@@ -39,18 +39,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>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ class SelectAuthorModalContent extends Component {
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value.toLowerCase() });
|
||||
this.setState({ filter: value });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -43,6 +43,7 @@ class SelectAuthorModalContent extends Component {
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -66,7 +67,7 @@ class SelectAuthorModalContent extends Component {
|
||||
<Scroller className={styles.scroller}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return item.authorName.toLowerCase().includes(filter) ?
|
||||
return item.authorName.toLowerCase().includes(filterLower) ?
|
||||
(
|
||||
<SelectAuthorRow
|
||||
key={item.id}
|
||||
|
||||
@@ -52,7 +52,7 @@ class SelectBookModalContent extends Component {
|
||||
// Listeners
|
||||
|
||||
onFilterChange = ({ value }) => {
|
||||
this.setState({ filter: value.toLowerCase() });
|
||||
this.setState({ filter: value });
|
||||
};
|
||||
|
||||
//
|
||||
@@ -68,6 +68,7 @@ class SelectBookModalContent extends Component {
|
||||
} = this.props;
|
||||
|
||||
const filter = this.state.filter;
|
||||
const filterLower = filter.toLowerCase();
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
@@ -101,7 +102,7 @@ class SelectBookModalContent extends Component {
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return item.title.toLowerCase().includes(filter) ?
|
||||
return item.title.toLowerCase().includes(filterLower) ?
|
||||
(
|
||||
<SelectBookRow
|
||||
key={item.id}
|
||||
|
||||
@@ -295,11 +295,11 @@ class InteractiveImportModalContent extends Component {
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
|
||||
|
||||
const bulkSelectOptions = [
|
||||
{ key: SELECT, value: 'Select...', disabled: true },
|
||||
{ key: BOOK, value: 'Select Book' },
|
||||
{ key: EDITION, value: 'Select Edition' },
|
||||
{ key: QUALITY, value: 'Select Quality' },
|
||||
{ key: RELEASE_GROUP, value: 'Select ReleaseGroup' }
|
||||
{ key: SELECT, value: translate('SelectDropdown'), disabled: true },
|
||||
{ key: BOOK, value: translate('SelectBook') },
|
||||
{ key: EDITION, value: translate('SelectEdition') },
|
||||
{ key: QUALITY, value: translate('SelectQuality') },
|
||||
{ key: RELEASE_GROUP, value: translate('SelectReleaseGroup') }
|
||||
];
|
||||
|
||||
if (allowAuthorChange) {
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quality {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.customFormatScore {
|
||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class InteractiveSearchRow extends Component {
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<BookQuality quality={quality} />
|
||||
<BookQuality quality={quality} showRevision={true} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
|
||||
@@ -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: 'This condition matches using Regular Expressions. Note that the characters <code>\\^$.|?*+()[{</code> have special meanings and need escaping with a <code>\\</code>' }} />
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -61,8 +61,12 @@ function DownloadClientOptions(props) {
|
||||
legend={translate('FailedDownloadHandling')}
|
||||
>
|
||||
<Form>
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>{translate('RedownloadFailed')}</FormLabel>
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailed')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
@@ -72,7 +76,28 @@ function DownloadClientOptions(props) {
|
||||
{...settings.autoRedownloadFailed}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
settings.autoRedownloadFailed.value ?
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
size={sizes.MEDIUM}
|
||||
>
|
||||
<FormLabel>{translate('AutoRedownloadFailedFromInteractiveSearch')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="autoRedownloadFailedFromInteractiveSearch"
|
||||
helpText={translate('AutoRedownloadFailedFromInteractiveSearchHelpText')}
|
||||
onChange={onInputChange}
|
||||
{...settings.autoRedownloadFailedFromInteractiveSearch}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</Form>
|
||||
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('RemoveDownloadsAlert')}
|
||||
</Alert>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -14,12 +14,17 @@ function PendingChangesModal(props) {
|
||||
isOpen,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
bindShortcut
|
||||
bindShortcut,
|
||||
unbindShortcut
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
bindShortcut('enter', onConfirm);
|
||||
}, [bindShortcut, onConfirm]);
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
|
||||
return () => unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [bindShortcut, unbindShortcut, isOpen, onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -60,7 +65,8 @@ PendingChangesModal.propTypes = {
|
||||
kind: PropTypes.oneOf(kinds.all),
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired
|
||||
bindShortcut: PropTypes.func.isRequired,
|
||||
unbindShortcut: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
PendingChangesModal.defaultProps = {
|
||||
|
||||
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchDelayProfiles, fetchDownloadClients, fetchImportLists, fetchIndexers, fetchNotifications, fetchReleaseProfiles } from 'Store/Actions/settingsActions';
|
||||
import { fetchTagDetails } from 'Store/Actions/tagActions';
|
||||
import { fetchTagDetails, fetchTags } from 'Store/Actions/tagActions';
|
||||
import Tags from './Tags';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -25,6 +25,7 @@ function createMapStateToProps() {
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchTags: fetchTags,
|
||||
dispatchFetchTagDetails: fetchTagDetails,
|
||||
dispatchFetchDelayProfiles: fetchDelayProfiles,
|
||||
dispatchFetchImportLists: fetchImportLists,
|
||||
@@ -41,6 +42,7 @@ class MetadatasConnector extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
dispatchFetchTags,
|
||||
dispatchFetchTagDetails,
|
||||
dispatchFetchDelayProfiles,
|
||||
dispatchFetchImportLists,
|
||||
@@ -50,6 +52,7 @@ class MetadatasConnector extends Component {
|
||||
dispatchFetchDownloadClients
|
||||
} = this.props;
|
||||
|
||||
dispatchFetchTags();
|
||||
dispatchFetchTagDetails();
|
||||
dispatchFetchDelayProfiles();
|
||||
dispatchFetchImportLists();
|
||||
@@ -72,6 +75,7 @@ class MetadatasConnector extends Component {
|
||||
}
|
||||
|
||||
MetadatasConnector.propTypes = {
|
||||
dispatchFetchTags: PropTypes.func.isRequired,
|
||||
dispatchFetchTagDetails: PropTypes.func.isRequired,
|
||||
dispatchFetchDelayProfiles: PropTypes.func.isRequired,
|
||||
dispatchFetchImportLists: PropTypes.func.isRequired,
|
||||
|
||||
@@ -21,19 +21,19 @@ export const firstDayOfWeekOptions = [
|
||||
];
|
||||
|
||||
export const weekColumnOptions = [
|
||||
{ key: 'ddd M/D', value: 'Tue 3/25' },
|
||||
{ key: 'ddd MM/DD', value: 'Tue 03/25' },
|
||||
{ key: 'ddd D/M', value: 'Tue 25/3' },
|
||||
{ key: 'ddd DD/MM', value: 'Tue 25/03' }
|
||||
{ key: 'ddd M/D', value: 'Tue 3/25', hint: 'ddd M/D' },
|
||||
{ key: 'ddd MM/DD', value: 'Tue 03/25', hint: 'ddd MM/DD' },
|
||||
{ key: 'ddd D/M', value: 'Tue 25/3', hint: 'ddd D/M' },
|
||||
{ key: 'ddd DD/MM', value: 'Tue 25/03', hint: 'ddd DD/MM' }
|
||||
];
|
||||
|
||||
const shortDateFormatOptions = [
|
||||
{ key: 'MMM D YYYY', value: 'Mar 25 2014' },
|
||||
{ key: 'DD MMM YYYY', value: '25 Mar 2014' },
|
||||
{ key: 'MM/D/YYYY', value: '03/25/2014' },
|
||||
{ key: 'MM/DD/YYYY', value: '03/25/2014' },
|
||||
{ key: 'DD/MM/YYYY', value: '25/03/2014' },
|
||||
{ key: 'YYYY-MM-DD', value: '2014-03-25' }
|
||||
{ key: 'MMM D YYYY', value: 'Mar 25 2014', hint: 'MMM D YYYY' },
|
||||
{ key: 'DD MMM YYYY', value: '25 Mar 2014', hint: 'DD MMM YYYY' },
|
||||
{ key: 'MM/D/YYYY', value: '03/25/2014', hint: 'MM/D/YYYY' },
|
||||
{ key: 'MM/DD/YYYY', value: '03/25/2014', hint: 'MM/DD/YYYY' },
|
||||
{ key: 'DD/MM/YYYY', value: '25/03/2014', hint: 'DD/MM/YYYY' },
|
||||
{ key: 'YYYY-MM-DD', value: '2014-03-25', hint: 'YYYY-MM-DD' }
|
||||
];
|
||||
|
||||
const longDateFormatOptions = [
|
||||
@@ -223,6 +223,13 @@ class UISettings extends Component {
|
||||
helpTextWarning={translate('UILanguageHelpTextWarning')}
|
||||
onChange={onInputChange}
|
||||
{...settings.uiLanguage}
|
||||
errors={
|
||||
languages.some((language) => language.key === settings.uiLanguage.value) ?
|
||||
settings.uiLanguage.errors :
|
||||
[
|
||||
...settings.uiLanguage.errors,
|
||||
{ message: translate('InvalidUILanguage') }
|
||||
]}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FieldSet>
|
||||
|
||||
@@ -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';
|
||||
@@ -33,6 +35,7 @@ export const CANCEL_TEST_DOWNLOAD_CLIENT = 'settings/downloadClients/cancelTestD
|
||||
export const TEST_ALL_DOWNLOAD_CLIENTS = 'settings/downloadClients/testAllDownloadClients';
|
||||
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
|
||||
@@ -49,6 +52,7 @@ export const cancelTestDownloadClient = createThunk(CANCEL_TEST_DOWNLOAD_CLIENT)
|
||||
export const testAllDownloadClients = createThunk(TEST_ALL_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 {
|
||||
@@ -88,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();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
@@ -121,7 +132,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';
|
||||
@@ -36,6 +38,7 @@ export const CANCEL_TEST_INDEXER = 'settings/indexers/cancelTestIndexer';
|
||||
export const TEST_ALL_INDEXERS = 'settings/indexers/testAllIndexers';
|
||||
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,6 +56,7 @@ export const cancelTestIndexer = createThunk(CANCEL_TEST_INDEXER);
|
||||
export const testAllIndexers = createThunk(TEST_ALL_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 {
|
||||
@@ -92,7 +96,14 @@ export default {
|
||||
isTesting: false,
|
||||
isTestingAll: false,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
pendingChanges: {},
|
||||
sortKey: 'name',
|
||||
sortDirection: sortDirections.ASCENDING,
|
||||
sortPredicates: {
|
||||
name: function(item) {
|
||||
return item.name.toLowerCase();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
//
|
||||
@@ -140,7 +151,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;
|
||||
@@ -151,7 +168,10 @@ export default {
|
||||
};
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
}
|
||||
},
|
||||
|
||||
[SET_MANAGE_INDEXERS_SORT]: createSetClientSideCollectionSortReducer(section)
|
||||
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -41,6 +41,14 @@ export const defaultState = {
|
||||
},
|
||||
|
||||
columns: [
|
||||
{
|
||||
name: 'select',
|
||||
columnLabel: 'Select',
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
isHidden: true
|
||||
},
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
|
||||
@@ -158,7 +158,7 @@ export const defaultState = {
|
||||
bookFileCount: function(item) {
|
||||
const { statistics = {} } = item;
|
||||
|
||||
return statistics.bookCount || 0;
|
||||
return statistics.bookFileCount || 0;
|
||||
},
|
||||
|
||||
ratings: function(item) {
|
||||
|
||||
@@ -84,11 +84,6 @@ export const defaultState = {
|
||||
label: 'Source Title',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: 'Source Title',
|
||||
isVisible: false
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: 'Custom Format Score',
|
||||
|
||||
@@ -371,13 +371,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;
|
||||
|
||||
@@ -395,7 +396,8 @@ export const actionHandlers = handleThunks({
|
||||
ids,
|
||||
remove,
|
||||
blocklist,
|
||||
skipRedownload
|
||||
skipRedownload,
|
||||
changeCategory
|
||||
} = payload;
|
||||
|
||||
dispatch(batchActions([
|
||||
@@ -411,7 +413,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',
|
||||
data: JSON.stringify({ ids })
|
||||
|
||||
@@ -22,9 +22,9 @@ class About extends Component {
|
||||
isNetCore,
|
||||
isDocker,
|
||||
runtimeVersion,
|
||||
migrationVersion,
|
||||
databaseVersion,
|
||||
databaseType,
|
||||
migrationVersion,
|
||||
appData,
|
||||
startupPath,
|
||||
mode,
|
||||
@@ -66,13 +66,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
|
||||
@@ -114,9 +114,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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
@@ -9,8 +10,12 @@ import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||
import { align, icons, kinds, sortDirections } from 'Helpers/Props';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
|
||||
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
|
||||
|
||||
@@ -23,10 +28,43 @@ class UnmappedFilesTable extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
scroller: null
|
||||
scroller: null,
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isDeleting,
|
||||
deleteError
|
||||
} = this.props;
|
||||
|
||||
if (sortKey !== prevProps.sortKey ||
|
||||
sortDirection !== prevProps.sortDirection ||
|
||||
hasDifferentItemsOrOrder(prevProps.items, items)
|
||||
) {
|
||||
this.setSelectedState();
|
||||
}
|
||||
|
||||
const hasFinishedDeleting = prevProps.isDeleting &&
|
||||
!isDeleting &&
|
||||
!deleteError;
|
||||
|
||||
if (hasFinishedDeleting) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
@@ -34,6 +72,68 @@ class UnmappedFilesTable extends Component {
|
||||
this.setState({ scroller: ref });
|
||||
};
|
||||
|
||||
getSelectedIds = () => {
|
||||
if (this.state.allUnselected) {
|
||||
return [];
|
||||
}
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
};
|
||||
|
||||
setSelectedState() {
|
||||
const {
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const newSelectedState = {};
|
||||
|
||||
items.forEach((file) => {
|
||||
const isItemSelected = selectedState[file.id];
|
||||
|
||||
if (isItemSelected) {
|
||||
newSelectedState[file.id] = isItemSelected;
|
||||
} else {
|
||||
newSelectedState[file.id] = false;
|
||||
}
|
||||
});
|
||||
|
||||
const selectedCount = getSelectedIds(newSelectedState).length;
|
||||
const newStateCount = Object.keys(newSelectedState).length;
|
||||
let isAllSelected = false;
|
||||
let isAllUnselected = false;
|
||||
|
||||
if (selectedCount === 0) {
|
||||
isAllUnselected = true;
|
||||
} else if (selectedCount === newStateCount) {
|
||||
isAllSelected = true;
|
||||
}
|
||||
|
||||
this.setState({ selectedState: newSelectedState, allSelected: isAllSelected, allUnselected: isAllUnselected });
|
||||
}
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
};
|
||||
|
||||
onSelectAllPress = () => {
|
||||
this.onSelectAllChange({ value: !this.state.allSelected });
|
||||
};
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
};
|
||||
|
||||
onDeleteUnmappedFilesPress = () => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
this.props.deleteUnmappedFiles(selectedIds);
|
||||
};
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
@@ -41,6 +141,10 @@ class UnmappedFilesTable extends Component {
|
||||
deleteUnmappedFile
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
@@ -51,6 +155,8 @@ class UnmappedFilesTable extends Component {
|
||||
<UnmappedFilesTableRow
|
||||
key={item.id}
|
||||
columns={columns}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
deleteUnmappedFile={deleteUnmappedFile}
|
||||
{...item}
|
||||
/>
|
||||
@@ -63,6 +169,7 @@ class UnmappedFilesTable extends Component {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
isDeleting,
|
||||
error,
|
||||
items,
|
||||
columns,
|
||||
@@ -72,13 +179,19 @@ class UnmappedFilesTable extends Component {
|
||||
onSortPress,
|
||||
isScanningFolders,
|
||||
onAddMissingAuthorsPress,
|
||||
deleteUnmappedFiles,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
scroller
|
||||
scroller,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const selectedTrackFileIds = this.getSelectedIds();
|
||||
|
||||
return (
|
||||
<PageContent title={translate('UnmappedFiles')}>
|
||||
<PageToolbar>
|
||||
@@ -90,6 +203,13 @@ class UnmappedFilesTable extends Component {
|
||||
isSpinning={isScanningFolders}
|
||||
onPress={onAddMissingAuthorsPress}
|
||||
/>
|
||||
<PageToolbarButton
|
||||
label={translate('DeleteSelected')}
|
||||
iconName={icons.DELETE}
|
||||
isDisabled={selectedTrackFileIds.length === 0}
|
||||
isSpinning={isDeleting}
|
||||
onPress={this.onDeleteUnmappedFilesPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
@@ -117,9 +237,9 @@ class UnmappedFilesTable extends Component {
|
||||
|
||||
{
|
||||
isPopulated && !error && !items.length &&
|
||||
<div>
|
||||
<Alert kind={kinds.INFO}>
|
||||
Success! My work is done, all files on disk are matched to known books.
|
||||
</div>
|
||||
</Alert>
|
||||
}
|
||||
|
||||
{
|
||||
@@ -138,8 +258,12 @@ class UnmappedFilesTable extends Component {
|
||||
sortDirection={sortDirection}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
onSortPress={onSortPress}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
/>
|
||||
@@ -153,6 +277,8 @@ class UnmappedFilesTable extends Component {
|
||||
UnmappedFilesTable.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
@@ -161,6 +287,7 @@ UnmappedFilesTable.propTypes = {
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||
deleteUnmappedFiles: PropTypes.func.isRequired,
|
||||
isScanningFolders: PropTypes.bool.isRequired,
|
||||
onAddMissingAuthorsPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { deleteBookFile, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
|
||||
import { deleteBookFile, deleteBookFiles, fetchBookFiles, setBookFilesSort, setBookFilesTableOption } from 'Store/Actions/bookFileActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
@@ -28,7 +28,9 @@ function createMapStateToProps() {
|
||||
items,
|
||||
...otherProps
|
||||
} = bookFiles;
|
||||
|
||||
const unmappedFiles = _.filter(items, { bookId: 0 });
|
||||
|
||||
return {
|
||||
items: unmappedFiles,
|
||||
...otherProps,
|
||||
@@ -57,6 +59,10 @@ function createMapDispatchToProps(dispatch, props) {
|
||||
dispatch(deleteBookFile({ id }));
|
||||
},
|
||||
|
||||
deleteUnmappedFiles(bookFileIds) {
|
||||
dispatch(deleteBookFiles({ bookFileIds }));
|
||||
},
|
||||
|
||||
onAddMissingAuthorsPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RESCAN_FOLDERS,
|
||||
@@ -106,7 +112,8 @@ UnmappedFilesTableConnector.propTypes = {
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
fetchUnmappedFiles: PropTypes.func.isRequired,
|
||||
deleteUnmappedFile: PropTypes.func.isRequired
|
||||
deleteUnmappedFile: PropTypes.func.isRequired,
|
||||
deleteUnmappedFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withCurrentPage(
|
||||
|
||||
@@ -4,6 +4,7 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||
import VirtualTableSelectAllHeaderCell from 'Components/Table/VirtualTableSelectAllHeaderCell';
|
||||
import { icons } from 'Helpers/Props';
|
||||
// import hasGrowableColumns from './hasGrowableColumns';
|
||||
import styles from './UnmappedFilesTableHeader.css';
|
||||
@@ -12,6 +13,9 @@ function UnmappedFilesTableHeader(props) {
|
||||
const {
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
allSelected,
|
||||
allUnselected,
|
||||
onSelectAllChange,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
@@ -30,6 +34,17 @@ function UnmappedFilesTableHeader(props) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectAllHeaderCell
|
||||
key={name}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'actions') {
|
||||
return (
|
||||
<VirtualTableHeaderCell
|
||||
@@ -71,6 +86,9 @@ function UnmappedFilesTableHeader(props) {
|
||||
|
||||
UnmappedFilesTableHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
allSelected: PropTypes.bool.isRequired,
|
||||
allUnselected: PropTypes.bool.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -20,3 +20,9 @@
|
||||
|
||||
flex: 0 0 100px;
|
||||
}
|
||||
|
||||
.checkInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'actions': string;
|
||||
'checkInput': string;
|
||||
'dateAdded': string;
|
||||
'path': string;
|
||||
'quality': string;
|
||||
|
||||
@@ -6,6 +6,7 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
@@ -69,7 +70,9 @@ class UnmappedFilesTableRow extends Component {
|
||||
size,
|
||||
dateAdded,
|
||||
quality,
|
||||
columns
|
||||
columns,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
|
||||
@@ -93,6 +96,19 @@ class UnmappedFilesTableRow extends Component {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'select') {
|
||||
return (
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.checkInput}
|
||||
id={id}
|
||||
key={name}
|
||||
isSelected={isSelected}
|
||||
isDisabled={false}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'path') {
|
||||
return (
|
||||
<VirtualTableRowCell
|
||||
@@ -208,6 +224,8 @@ UnmappedFilesTableRow.propTypes = {
|
||||
quality: PropTypes.object.isRequired,
|
||||
dateAdded: PropTypes.string.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
deleteUnmappedFile: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -25,20 +25,18 @@ export async function fetchTranslations(): Promise<boolean> {
|
||||
|
||||
export default function translate(
|
||||
key: string,
|
||||
tokens: Record<string, string | number | boolean> = { appName: 'Readarr' }
|
||||
tokens: Record<string, string | number | boolean> = {}
|
||||
) {
|
||||
const translation = translations[key] || key;
|
||||
|
||||
if (tokens) {
|
||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||
Object.values(tokens).forEach((value, index) => {
|
||||
tokens[index] = value;
|
||||
});
|
||||
tokens.appName = 'Readarr';
|
||||
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
// Fallback to the old behaviour for translations not yet updated to use named tokens
|
||||
Object.values(tokens).forEach((value, index) => {
|
||||
tokens[index] = value;
|
||||
});
|
||||
|
||||
return translation;
|
||||
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
|
||||
String(tokens[tokenMatch] ?? match)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<PackageVersion Include="DryIoc.Microsoft.DependencyInjection" Version="6.2.0" />
|
||||
<PackageVersion Include="Equ" Version="2.3.0" />
|
||||
<PackageVersion Include="FluentAssertions" Version="5.10.3" />
|
||||
<PackageVersion Include="Polly" Version="8.2.0" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.SQLite" Version="3.3.2.9" />
|
||||
<PackageVersion Include="Servarr.FluentMigrator.Runner.Postgres" Version="3.3.2.9" />
|
||||
|
||||
@@ -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 Readarr.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()
|
||||
{
|
||||
|
||||
@@ -124,6 +124,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()
|
||||
{
|
||||
|
||||
@@ -103,31 +103,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -82,7 +82,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -101,7 +101,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook2));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -172,7 +172,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>())).Throws(new Exception());
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), null)).Throws(new Exception());
|
||||
|
||||
var result = await Subject.ProcessDecisions(decisions);
|
||||
|
||||
@@ -201,7 +201,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook, new Rejection("Failure!", RejectionType.Temporary)));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Never());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -242,11 +242,11 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), null))
|
||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteBook>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -260,12 +260,12 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
decisions.Add(new DownloadDecision(remoteBook2));
|
||||
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null))
|
||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteBook>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -278,7 +278,7 @@ namespace NzbDrone.Core.Test.Download.DownloadApprovedReportsTests
|
||||
decisions.Add(new DownloadDecision(remoteBook));
|
||||
|
||||
Mocker.GetMock<IDownloadService>()
|
||||
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>()))
|
||||
.Setup(s => s.DownloadReport(It.IsAny<RemoteBook>(), null))
|
||||
.Throws(new ReleaseUnavailableException(remoteBook.Release, "That 404 Error is not just a Quirk"));
|
||||
|
||||
var result = await Subject.ProcessDecisions(decisions);
|
||||
|
||||
@@ -454,6 +454,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests
|
||||
|
||||
[TestCase("0")]
|
||||
[TestCase("15d")]
|
||||
[TestCase("")]
|
||||
[TestCase(null)]
|
||||
public void should_set_history_removes_completed_downloads_false(string historyRetention)
|
||||
{
|
||||
_config.Misc.history_retention = historyRetention;
|
||||
|
||||
@@ -83,7 +83,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
var mock = WithUsenetClient();
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
VerifyEventPublished<BookGrabbedEvent>();
|
||||
}
|
||||
@@ -94,7 +94,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
var mock = WithUsenetClient();
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()));
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
mock.Verify(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
}
|
||||
@@ -106,7 +106,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
||||
.Throws(new WebException());
|
||||
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
VerifyEventNotPublished<BookGrabbedEvent>();
|
||||
}
|
||||
@@ -121,7 +121,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
throw new ReleaseDownloadException(v.Release, "Error", new WebException());
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
|
||||
@@ -141,7 +141,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
|
||||
@@ -161,7 +161,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(),
|
||||
@@ -175,7 +175,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()))
|
||||
.Throws(new DownloadClientException("Some Error"));
|
||||
|
||||
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||
@@ -191,7 +191,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||
@@ -200,7 +200,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
[Test]
|
||||
public void should_not_attempt_download_if_client_isnt_configured()
|
||||
{
|
||||
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||
VerifyEventNotPublished<BookGrabbedEvent>();
|
||||
@@ -222,7 +222,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
}
|
||||
});
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
|
||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
@@ -235,7 +235,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
var mockTorrent = WithTorrentClient();
|
||||
var mockUsenet = WithUsenetClient();
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
@@ -249,7 +249,7 @@ namespace NzbDrone.Core.Test.Download
|
||||
|
||||
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteBook>(), It.IsAny<IIndexer>()), Times.Never());
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||
|
||||
results.Should().NotBeEmpty();
|
||||
Mocker.GetMock<IMakeDownloadDecision>()
|
||||
.Verify(v => v.GetRssDecision(It.Is<List<ReleaseInfo>>(d => d.Count == 0)), Times.Never());
|
||||
.Verify(v => v.GetRssDecision(It.Is<List<ReleaseInfo>>(d => d.Count == 0), It.IsAny<bool>()), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace NzbDrone.Core.Test.IndexerTests.FileListTests
|
||||
|
||||
Mocker.GetMock<IHttpClient>()
|
||||
.Setup(o => o.ExecuteAsync(It.Is<HttpRequest>(v => v.Method == HttpMethod.Get)))
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader(), recentFeed)));
|
||||
.Returns<HttpRequest>(r => Task.FromResult(new HttpResponse(r, new HttpHeader { { "Content-Type", "application/json" } }, recentFeed)));
|
||||
|
||||
var releases = await Subject.FetchRecent();
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Localization;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Localization
|
||||
{
|
||||
[TestFixture]
|
||||
public class LocalizationServiceFixture : CoreTest<LocalizationService>
|
||||
{
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.English);
|
||||
|
||||
Mocker.GetMock<IAppFolderInfo>().Setup(m => m.StartUpFolder).Returns(TestContext.CurrentContext.TestDirectory);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_string_in_dictionary_if_lang_exists_and_string_exists()
|
||||
{
|
||||
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||
|
||||
localizedString.Should().Be("UI Language");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_string_in_french()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns((int)Language.French);
|
||||
|
||||
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||
|
||||
localizedString.Should().Be("Langue de l'IU");
|
||||
|
||||
ExceptionVerification.ExpectedErrors(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_get_string_in_default_dictionary_if_unknown_language_and_string_exists()
|
||||
{
|
||||
Mocker.GetMock<IConfigService>().Setup(m => m.UILanguage).Returns(0);
|
||||
var localizedString = Subject.GetLocalizedString("UiLanguage");
|
||||
|
||||
localizedString.Should().Be("UI Language");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_argument_if_string_doesnt_exists()
|
||||
{
|
||||
var localizedString = Subject.GetLocalizedString("badString");
|
||||
|
||||
localizedString.Should().Be("badString");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_argument_if_string_doesnt_exists_default_lang()
|
||||
{
|
||||
var localizedString = Subject.GetLocalizedString("badString");
|
||||
|
||||
localizedString.Should().Be("badString");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_if_empty_string_passed()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => Subject.GetLocalizedString(""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_throw_if_null_string_passed()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => Subject.GetLocalizedString(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ using NzbDrone.Core.Test.Framework;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2023-12-31 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2024-03-15 00:00:00Z")]
|
||||
public class BookInfoProxyFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
private MetadataProfile _metadataProfile;
|
||||
|
||||
@@ -15,7 +15,7 @@ using NzbDrone.Test.Common;
|
||||
namespace NzbDrone.Core.Test.MetadataSource.Goodreads
|
||||
{
|
||||
[TestFixture]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2023-12-31 00:00:00Z")]
|
||||
[Ignore("Waiting for metadata to be back again", Until = "2024-03-15 00:00:00Z")]
|
||||
public class BookInfoProxySearchFixture : CoreTest<BookInfoProxy>
|
||||
{
|
||||
[SetUp]
|
||||
|
||||
@@ -166,6 +166,7 @@ namespace NzbDrone.Core.Test.MusicTests.BookRepositoryTests
|
||||
|
||||
private EquivalencyAssertionOptions<Book> BookComparerOptions(EquivalencyAssertionOptions<Book> opts) => opts.ComparingByMembers<Book>()
|
||||
.Excluding(ctx => ctx.SelectedMemberInfo.MemberType.IsGenericType && ctx.SelectedMemberInfo.MemberType.GetGenericTypeDefinition() == typeof(LazyLoaded<>))
|
||||
.Excluding(x => x.AuthorId);
|
||||
.Excluding(x => x.AuthorId)
|
||||
.Excluding(x => x.ForeignEditionId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,23 @@ namespace NzbDrone.Core.Annotations
|
||||
public string Hint { get; set; }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
|
||||
public class FieldTokenAttribute : Attribute
|
||||
{
|
||||
public FieldTokenAttribute(TokenField field, string label = "", string token = "", object value = null)
|
||||
{
|
||||
Label = label;
|
||||
Field = field;
|
||||
Token = token;
|
||||
Value = value?.ToString();
|
||||
}
|
||||
|
||||
public string Label { get; set; }
|
||||
public TokenField Field { get; set; }
|
||||
public string Token { get; set; }
|
||||
public string Value { get; set; }
|
||||
}
|
||||
|
||||
public class FieldSelectOption
|
||||
{
|
||||
public int Value { get; set; }
|
||||
@@ -83,4 +100,11 @@ namespace NzbDrone.Core.Annotations
|
||||
ApiKey,
|
||||
UserName
|
||||
}
|
||||
|
||||
public enum TokenField
|
||||
{
|
||||
Label,
|
||||
HelpText,
|
||||
HelpTextWarning
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace NzbDrone.Core.Blocklisting
|
||||
public interface IBlocklistService
|
||||
{
|
||||
bool Blocklisted(int authorId, ReleaseInfo release);
|
||||
bool BlocklistedTorrentHash(int authorId, string hash);
|
||||
PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec);
|
||||
void Block(RemoteBook remoteEpisode, string message);
|
||||
void Delete(int id);
|
||||
@@ -36,30 +37,34 @@ namespace NzbDrone.Core.Blocklisting
|
||||
|
||||
public bool Blocklisted(int authorId, ReleaseInfo release)
|
||||
{
|
||||
var blocklistedByTitle = _blocklistRepository.BlocklistedByTitle(authorId, release.Title);
|
||||
|
||||
if (release.DownloadProtocol == DownloadProtocol.Torrent)
|
||||
{
|
||||
var torrentInfo = release as TorrentInfo;
|
||||
|
||||
if (torrentInfo == null)
|
||||
if (release is not TorrentInfo torrentInfo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (torrentInfo.InfoHash.IsNullOrWhiteSpace())
|
||||
if (torrentInfo.InfoHash.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
||||
.Any(b => SameTorrent(b, torrentInfo));
|
||||
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
||||
|
||||
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
||||
}
|
||||
|
||||
var blocklistedByTorrentInfohash = _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, torrentInfo.InfoHash);
|
||||
|
||||
return blocklistedByTorrentInfohash.Any(b => SameTorrent(b, torrentInfo));
|
||||
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||
.Where(b => b.Protocol == DownloadProtocol.Torrent)
|
||||
.Any(b => SameTorrent(b, torrentInfo));
|
||||
}
|
||||
|
||||
return blocklistedByTitle.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
||||
.Any(b => SameNzb(b, release));
|
||||
return _blocklistRepository.BlocklistedByTitle(authorId, release.Title)
|
||||
.Where(b => b.Protocol == DownloadProtocol.Usenet)
|
||||
.Any(b => SameNzb(b, release));
|
||||
}
|
||||
|
||||
public bool BlocklistedTorrentHash(int authorId, string hash)
|
||||
{
|
||||
return _blocklistRepository.BlocklistedByTorrentInfoHash(authorId, hash).Any(b =>
|
||||
b.TorrentInfoHash.Equals(hash, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
public PagingSpec<Blocklist> Paged(PagingSpec<Blocklist> pagingSpec)
|
||||
|
||||
@@ -219,11 +219,11 @@ namespace NzbDrone.Core.Books.Calibre
|
||||
double? seriesIndex = null;
|
||||
if (double.TryParse(serieslink?.Position, out var index))
|
||||
{
|
||||
_logger.Trace($"Parsed {serieslink?.Position} as {index}");
|
||||
_logger.Trace("Parsed '{0}' as '{1}'", serieslink.Position, index);
|
||||
seriesIndex = index;
|
||||
}
|
||||
|
||||
_logger.Trace($"Book: {book} Series: {series?.Title}, Position: {seriesIndex}");
|
||||
_logger.Trace("Book: {0} Series: {1}, Position: {2}", book, series?.Title, seriesIndex);
|
||||
|
||||
var cover = edition.Images.FirstOrDefault(x => x.CoverType == MediaCoverTypes.Cover);
|
||||
string image = null;
|
||||
@@ -276,7 +276,9 @@ namespace NzbDrone.Core.Books.Calibre
|
||||
|
||||
var updatedPath = GetOriginalFormat(updated.Formats);
|
||||
|
||||
if (updatedPath != null && updatedPath != file.Path)
|
||||
_logger.Trace("File path from Calibre: '{0}'", updatedPath);
|
||||
|
||||
if (updatedPath.IsNotNullOrWhiteSpace() && updatedPath != file.Path)
|
||||
{
|
||||
_rootFolderWatchingService.ReportFileSystemChangeBeginning(updatedPath);
|
||||
file.Path = updatedPath;
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace NzbDrone.Core.Books
|
||||
// These are metadata entries
|
||||
public int AuthorMetadataId { get; set; }
|
||||
public string ForeignBookId { get; set; }
|
||||
public string ForeignEditionId { get; set; }
|
||||
public string TitleSlug { get; set; }
|
||||
public string Title { get; set; }
|
||||
public DateTime? ReleaseDate { get; set; }
|
||||
@@ -33,6 +34,7 @@ namespace NzbDrone.Core.Books
|
||||
public List<string> Genres { get; set; }
|
||||
public List<int> RelatedBooks { get; set; }
|
||||
public Ratings Ratings { get; set; }
|
||||
public DateTime? LastSearchTime { get; set; }
|
||||
|
||||
// These are Readarr generated/config
|
||||
public string CleanTitle { get; set; }
|
||||
@@ -71,6 +73,7 @@ namespace NzbDrone.Core.Books
|
||||
public override void UseMetadataFrom(Book other)
|
||||
{
|
||||
ForeignBookId = other.ForeignBookId;
|
||||
ForeignEditionId = other.ForeignEditionId;
|
||||
TitleSlug = other.TitleSlug;
|
||||
Title = other.Title;
|
||||
ReleaseDate = other.ReleaseDate;
|
||||
@@ -88,6 +91,7 @@ namespace NzbDrone.Core.Books
|
||||
Monitored = other.Monitored;
|
||||
AnyEditionOk = other.AnyEditionOk;
|
||||
LastInfoSync = other.LastInfoSync;
|
||||
LastSearchTime = other.LastSearchTime;
|
||||
Added = other.Added;
|
||||
AddOptions = other.AddOptions;
|
||||
}
|
||||
@@ -95,6 +99,7 @@ namespace NzbDrone.Core.Books
|
||||
public override void ApplyChanges(Book other)
|
||||
{
|
||||
ForeignBookId = other.ForeignBookId;
|
||||
ForeignEditionId = other.ForeignEditionId;
|
||||
AddOptions = other.AddOptions;
|
||||
Monitored = other.Monitored;
|
||||
AnyEditionOk = other.AnyEditionOk;
|
||||
|
||||
@@ -225,7 +225,12 @@ namespace NzbDrone.Core.Books
|
||||
public Author UpdateAuthor(Author author)
|
||||
{
|
||||
_cache.Clear();
|
||||
|
||||
var storedAuthor = GetAuthor(author.Id);
|
||||
|
||||
// Never update AddOptions when updating an author, keep it the same as the existing stored author.
|
||||
author.AddOptions = storedAuthor.AddOptions;
|
||||
|
||||
var updatedAuthor = _authorRepository.Update(author);
|
||||
_eventAggregator.PublishEvent(new AuthorEditedEvent(updatedAuthor, storedAuthor));
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace NzbDrone.Core.Books
|
||||
Book UpdateBook(Book book);
|
||||
void SetBookMonitored(int bookId, bool monitored);
|
||||
void SetMonitored(IEnumerable<int> ids, bool monitored);
|
||||
void UpdateLastSearchTime(List<Book> books);
|
||||
PagingSpec<Book> BooksWithoutFiles(PagingSpec<Book> pagingSpec);
|
||||
List<Book> BooksBetweenDates(DateTime start, DateTime end, bool includeUnmonitored);
|
||||
List<Book> AuthorBooksBetweenDates(Author author, DateTime start, DateTime end, bool includeUnmonitored);
|
||||
@@ -303,6 +304,11 @@ namespace NzbDrone.Core.Books
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateLastSearchTime(List<Book> books)
|
||||
{
|
||||
_bookRepository.SetFields(books, b => b.LastSearchTime);
|
||||
}
|
||||
|
||||
public void Handle(AuthorDeletedEvent message)
|
||||
{
|
||||
var books = GetBooksByAuthorMetadataId(message.Author.AuthorMetadataId);
|
||||
|
||||
@@ -329,8 +329,8 @@ namespace NzbDrone.Core.Configuration
|
||||
return;
|
||||
}
|
||||
|
||||
// If SSL is enabled and a cert hash is still in the config file disable SSL
|
||||
if (EnableSsl && GetValue("SslCertHash", null).IsNotNullOrWhiteSpace())
|
||||
// If SSL is enabled and a cert hash is still in the config file or cert path is empty disable SSL
|
||||
if (EnableSsl && (GetValue("SslCertHash", null).IsNotNullOrWhiteSpace() || SslCertPath.IsNullOrWhiteSpace()))
|
||||
{
|
||||
SetValue("EnableSsl", false);
|
||||
}
|
||||
|
||||
@@ -144,6 +144,13 @@ namespace NzbDrone.Core.Configuration
|
||||
set { SetValue("AutoRedownloadFailed", value); }
|
||||
}
|
||||
|
||||
public bool AutoRedownloadFailedFromInteractiveSearch
|
||||
{
|
||||
get { return GetValueBoolean("AutoRedownloadFailedFromInteractiveSearch", true); }
|
||||
|
||||
set { SetValue("AutoRedownloadFailedFromInteractiveSearch", value); }
|
||||
}
|
||||
|
||||
public bool CreateEmptyAuthorFolders
|
||||
{
|
||||
get { return GetValueBoolean("CreateEmptyAuthorFolders", false); }
|
||||
|
||||
@@ -19,6 +19,7 @@ namespace NzbDrone.Core.Configuration
|
||||
//Completed/Failed Download Handling (Download client)
|
||||
bool EnableCompletedDownloadHandling { get; set; }
|
||||
bool AutoRedownloadFailed { get; set; }
|
||||
bool AutoRedownloadFailedFromInteractiveSearch { get; set; }
|
||||
|
||||
//Media Management
|
||||
bool AutoUnmonitorPreviouslyDownloadedBooks { get; set; }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Blocklisting;
|
||||
using NzbDrone.Core.Books;
|
||||
@@ -23,10 +24,12 @@ namespace NzbDrone.Core.CustomFormats
|
||||
public class CustomFormatCalculationService : ICustomFormatCalculationService
|
||||
{
|
||||
private readonly ICustomFormatService _formatService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public CustomFormatCalculationService(ICustomFormatService formatService)
|
||||
public CustomFormatCalculationService(ICustomFormatService formatService, Logger logger)
|
||||
{
|
||||
_formatService = formatService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public List<CustomFormat> ParseCustomFormat(RemoteBook remoteBook, long size)
|
||||
@@ -145,26 +148,30 @@ namespace NzbDrone.Core.CustomFormats
|
||||
return matches.OrderBy(x => x.Name).ToList();
|
||||
}
|
||||
|
||||
private static List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)
|
||||
private List<CustomFormat> ParseCustomFormat(BookFile bookFile, Author author, List<CustomFormat> allCustomFormats)
|
||||
{
|
||||
var sceneName = string.Empty;
|
||||
var releaseTitle = string.Empty;
|
||||
|
||||
if (bookFile.SceneName.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = bookFile.SceneName;
|
||||
_logger.Trace("Using scene name for release title: {0}", bookFile.SceneName);
|
||||
releaseTitle = bookFile.SceneName;
|
||||
}
|
||||
else if (bookFile.OriginalFilePath.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = bookFile.OriginalFilePath;
|
||||
_logger.Trace("Using original file path for release title: {0}", bookFile.OriginalFilePath);
|
||||
releaseTitle = bookFile.OriginalFilePath;
|
||||
}
|
||||
else if (bookFile.Path.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
sceneName = Path.GetFileName(bookFile.Path);
|
||||
_logger.Trace("Using path for release title: {0}", Path.GetFileName(bookFile.Path));
|
||||
releaseTitle = Path.GetFileName(bookFile.Path);
|
||||
}
|
||||
|
||||
var bookInfo = new ParsedBookInfo
|
||||
{
|
||||
AuthorName = author.Name,
|
||||
ReleaseTitle = sceneName,
|
||||
ReleaseTitle = releaseTitle,
|
||||
Quality = bookFile.Quality,
|
||||
ReleaseGroup = bookFile.ReleaseGroup
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace NzbDrone.Core.CustomFormats
|
||||
protected Regex _regex;
|
||||
protected string _raw;
|
||||
|
||||
[FieldDefinition(1, Label = "Regular Expression", HelpText = "Custom Format RegEx is Case Insensitive")]
|
||||
[FieldDefinition(1, Label = "CustomFormatsSpecificationRegularExpression", HelpText = "CustomFormatsSpecificationRegularExpressionHelpText")]
|
||||
public string Value
|
||||
{
|
||||
get => _raw;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.SQLite;
|
||||
using System.Text.RegularExpressions;
|
||||
using Dapper;
|
||||
using NLog;
|
||||
@@ -40,14 +41,7 @@ namespace NzbDrone.Core.Datastore
|
||||
{
|
||||
using (var db = _datamapperFactory())
|
||||
{
|
||||
if (db.ConnectionString.Contains(".db"))
|
||||
{
|
||||
return DatabaseType.SQLite;
|
||||
}
|
||||
else
|
||||
{
|
||||
return DatabaseType.PostgreSQL;
|
||||
}
|
||||
return db is SQLiteConnection ? DatabaseType.SQLite : DatabaseType.PostgreSQL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using FluentMigrator;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration
|
||||
{
|
||||
[Maintenance(MigrationStage.BeforeAll, TransactionBehavior.None)]
|
||||
public class DatabaseEngineVersionCheck : FluentMigrator.Migration
|
||||
{
|
||||
protected readonly Logger _logger;
|
||||
|
||||
public DatabaseEngineVersionCheck()
|
||||
{
|
||||
_logger = NzbDroneLogger.GetLogger(this);
|
||||
}
|
||||
|
||||
public override void Up()
|
||||
{
|
||||
IfDatabase("sqlite").Execute.WithConnection(LogSqliteVersion);
|
||||
IfDatabase("postgres").Execute.WithConnection(LogPostgresVersion);
|
||||
}
|
||||
|
||||
public override void Down()
|
||||
{
|
||||
// No-op
|
||||
}
|
||||
|
||||
private void LogSqliteVersion(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (var versionCmd = conn.CreateCommand())
|
||||
{
|
||||
versionCmd.Transaction = tran;
|
||||
versionCmd.CommandText = "SELECT sqlite_version();";
|
||||
|
||||
using (var reader = versionCmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var version = reader.GetString(0);
|
||||
|
||||
_logger.Info("SQLite {0}", version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void LogPostgresVersion(IDbConnection conn, IDbTransaction tran)
|
||||
{
|
||||
using (var versionCmd = conn.CreateCommand())
|
||||
{
|
||||
versionCmd.Transaction = tran;
|
||||
versionCmd.CommandText = "SHOW server_version";
|
||||
|
||||
using (var reader = versionCmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var version = reader.GetString(0);
|
||||
var cleanVersion = Regex.Replace(version, @"\(.*?\)", "");
|
||||
|
||||
_logger.Info("Postgres {0}", cleanVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user