mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-17 21:25:39 -04:00
New: Use Goodreads directly, allow multiple editions of a book (new DB required)
This commit is contained in:
37
frontend/src/InteractiveImport/Edition/SelectEditionModal.js
Normal file
37
frontend/src/InteractiveImport/Edition/SelectEditionModal.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import SelectEditionModalContentConnector from './SelectEditionModalContentConnector';
|
||||
|
||||
class SelectEditionModal extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<SelectEditionModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectEditionModal;
|
||||
@@ -0,0 +1,18 @@
|
||||
.modalBody {
|
||||
composes: modalBody from '~Components/Modal/ModalBody.css';
|
||||
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.filterInput {
|
||||
composes: input from '~Components/Form/TextInput.css';
|
||||
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.scroller {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import SelectEditionRow from './SelectEditionRow';
|
||||
import Alert from 'Components/Alert';
|
||||
import styles from './SelectEditionModalContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'book',
|
||||
label: 'Book',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'edition',
|
||||
label: 'Edition',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class SelectEditionModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
books,
|
||||
onEditionSelect,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Manual Import - Select Edition
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody
|
||||
className={styles.modalBody}
|
||||
scrollDirection={scrollDirections.NONE}
|
||||
>
|
||||
<Alert>
|
||||
Overrriding an edition here will <b>disable automatic edition selection</b> for that book in future.
|
||||
</Alert>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
books.map((item) => {
|
||||
return (
|
||||
<SelectEditionRow
|
||||
key={item.book.id}
|
||||
matchedEditionId={item.matchedEditionId}
|
||||
columns={columns}
|
||||
onEditionSelect={onEditionSelect}
|
||||
{...item.book}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionModalContent.propTypes = {
|
||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onEditionSelect: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default SelectEditionModalContent;
|
||||
@@ -0,0 +1,63 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
} from 'Store/Actions/interactiveImportActions';
|
||||
import SelectEditionModalContent from './SelectEditionModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateInteractiveImportItem,
|
||||
saveInteractiveImportItem
|
||||
};
|
||||
|
||||
class SelectEditionModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onEditionSelect = (bookId, editionId) => {
|
||||
const ids = this.props.importIdsByBook[bookId];
|
||||
|
||||
ids.forEach((id) => {
|
||||
this.props.updateInteractiveImportItem({
|
||||
id,
|
||||
editionId,
|
||||
disableReleaseSwitching: true,
|
||||
tracks: [],
|
||||
rejections: []
|
||||
});
|
||||
});
|
||||
|
||||
this.props.saveInteractiveImportItem({ id: ids });
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<SelectEditionModalContent
|
||||
{...this.props}
|
||||
onEditionSelect={this.onEditionSelect}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionModalContentConnector.propTypes = {
|
||||
importIdsByBook: PropTypes.object.isRequired,
|
||||
books: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
updateInteractiveImportItem: PropTypes.func.isRequired,
|
||||
saveInteractiveImportItem: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(SelectEditionModalContentConnector);
|
||||
@@ -0,0 +1,3 @@
|
||||
.albumRow {
|
||||
cursor: pointer;
|
||||
}
|
||||
125
frontend/src/InteractiveImport/Edition/SelectEditionRow.js
Normal file
125
frontend/src/InteractiveImport/Edition/SelectEditionRow.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import titleCase from 'Utilities/String/titleCase';
|
||||
|
||||
class SelectEditionRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.onEditionSelect(parseInt(name), parseInt(value));
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
matchedEditionId,
|
||||
title,
|
||||
disambiguation,
|
||||
editions,
|
||||
columns
|
||||
} = this.props;
|
||||
|
||||
const extendedTitle = disambiguation ? `${title} (${disambiguation})` : title;
|
||||
|
||||
const values = _.map(editions, (bookEdition) => {
|
||||
|
||||
let value = `${bookEdition.title}`;
|
||||
|
||||
if (bookEdition.disambiguation) {
|
||||
value = `${value} (${titleCase(bookEdition.disambiguation)})`;
|
||||
}
|
||||
|
||||
const extras = [];
|
||||
if (bookEdition.language) {
|
||||
extras.push(bookEdition.language);
|
||||
}
|
||||
if (bookEdition.publisher) {
|
||||
extras.push(bookEdition.publisher);
|
||||
}
|
||||
if (bookEdition.isbn13) {
|
||||
extras.push(bookEdition.isbn13);
|
||||
}
|
||||
if (bookEdition.format) {
|
||||
extras.push(bookEdition.format);
|
||||
}
|
||||
if (bookEdition.pageCount > 0) {
|
||||
extras.push(`${bookEdition.pageCount}p`);
|
||||
}
|
||||
|
||||
if (extras) {
|
||||
value = `${value} [${extras.join(', ')}]`;
|
||||
}
|
||||
|
||||
return {
|
||||
key: bookEdition.id,
|
||||
value
|
||||
};
|
||||
});
|
||||
|
||||
const sortedValues = _.orderBy(values, ['value']);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
{
|
||||
columns.map((column) => {
|
||||
const {
|
||||
name,
|
||||
isVisible
|
||||
} = column;
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (name === 'book') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{extendedTitle}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'edition') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name={id.toString()}
|
||||
values={sortedValues}
|
||||
value={matchedEditionId}
|
||||
onChange={this.onInputChange}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
}
|
||||
</TableRow>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectEditionRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
matchedEditionId: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string.isRequired,
|
||||
editions: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onEditionSelect: PropTypes.func.isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default SelectEditionRow;
|
||||
@@ -23,6 +23,7 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import SelectAuthorModal from 'InteractiveImport/Author/SelectAuthorModal';
|
||||
import SelectBookModal from 'InteractiveImport/Book/SelectBookModal';
|
||||
import SelectEditionModal from 'InteractiveImport/Edition/SelectEditionModal';
|
||||
import ConfirmImportModal from 'InteractiveImport/Confirmation/ConfirmImportModal';
|
||||
import InteractiveImportRow from './InteractiveImportRow';
|
||||
import styles from './InteractiveImportModalContent.css';
|
||||
@@ -79,6 +80,7 @@ const importModeOptions = [
|
||||
const SELECT = 'select';
|
||||
const AUTHOR = 'author';
|
||||
const BOOK = 'book';
|
||||
const EDITION = 'edition';
|
||||
const QUALITY = 'quality';
|
||||
|
||||
const replaceExistingFilesOptions = {
|
||||
@@ -112,7 +114,7 @@ class InteractiveImportModalContent extends Component {
|
||||
const selectedItems = _.filter(this.props.items, (x) => _.includes(selectedIds, x.id));
|
||||
|
||||
const inconsistent = _(selectedItems)
|
||||
.map((x) => ({ bookId: x.book ? x.book.id : 0, releaseId: x.bookReleaseId }))
|
||||
.map((x) => ({ bookId: x.book ? x.book.id : 0, releaseId: x.EditionId }))
|
||||
.groupBy('bookId')
|
||||
.mapValues((book) => _(book).groupBy((x) => x.releaseId).values().value().length)
|
||||
.values()
|
||||
@@ -273,6 +275,7 @@ class InteractiveImportModalContent extends Component {
|
||||
const bulkSelectOptions = [
|
||||
{ key: SELECT, value: 'Select...', disabled: true },
|
||||
{ key: BOOK, value: 'Select Book' },
|
||||
{ key: EDITION, value: 'Select Edition' },
|
||||
{ key: QUALITY, value: 'Select Quality' }
|
||||
];
|
||||
|
||||
@@ -469,6 +472,13 @@ class InteractiveImportModalContent extends Component {
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectEditionModal
|
||||
isOpen={selectModalOpen === EDITION}
|
||||
importIdsByBook={_.chain(items).filter((x) => x.album).groupBy((x) => x.book.id).mapValues((x) => x.map((y) => y.id)).value()}
|
||||
books={_.chain(items).filter((x) => x.book).keyBy((x) => x.book.id).mapValues((x) => ({ matchedEditionId: x.editionId, book: x.book })).values().value()}
|
||||
onModalClose={this.onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={selectModalOpen === QUALITY}
|
||||
ids={selectedIds}
|
||||
|
||||
@@ -128,6 +128,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
const {
|
||||
author,
|
||||
book,
|
||||
editionId,
|
||||
quality,
|
||||
disableReleaseSwitching
|
||||
} = item;
|
||||
@@ -151,6 +152,7 @@ class InteractiveImportModalContentConnector extends Component {
|
||||
path: item.path,
|
||||
authorId: author.id,
|
||||
bookId: book.id,
|
||||
editionId,
|
||||
quality,
|
||||
downloadId: this.props.downloadId,
|
||||
disableReleaseSwitching
|
||||
|
||||
Reference in New Issue
Block a user