New: Use Goodreads directly, allow multiple editions of a book (new DB required)

This commit is contained in:
ta264
2020-06-30 21:46:01 +01:00
parent d83d2548e5
commit 45d49117ca
178 changed files with 3332 additions and 1786 deletions

View 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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -0,0 +1,3 @@
.albumRow {
cursor: pointer;
}

View 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;

View File

@@ -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}

View File

@@ -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