mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-25 22:36:59 -04:00
Renames in Frontend
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableSelectCell from 'Components/Table/Cells/TableSelectCell';
|
||||
import BookQuality from 'Book/BookQuality';
|
||||
|
||||
function BookFileEditorRow(props) {
|
||||
const {
|
||||
id,
|
||||
path,
|
||||
quality,
|
||||
isSelected,
|
||||
onSelectedChange
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableSelectCell
|
||||
id={id}
|
||||
isSelected={isSelected}
|
||||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
<TableRowCell>
|
||||
{path}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<BookQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
BookFileEditorRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookFileEditorRow;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import BookFileEditorTableContentConnector from './BookFileEditorTableContentConnector';
|
||||
|
||||
function BookFileEditorTable(props) {
|
||||
const {
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<BookFileEditorTableContentConnector
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default BookFileEditorTable;
|
||||
@@ -0,0 +1,8 @@
|
||||
.actions {
|
||||
display: flex;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.selectInput {
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import BookFileEditorRow from './BookFileEditorRow';
|
||||
import styles from './BookFileEditorTableContent.css';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class BookFileEditorTableContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isConfirmDeleteModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
const selectedIds = getSelectedIds(this.state.selectedState);
|
||||
|
||||
return selectedIds.reduce((acc, id) => {
|
||||
const matchingItem = this.props.items.find((item) => item.id === id);
|
||||
|
||||
if (matchingItem && !acc.includes(matchingItem.bookFileId)) {
|
||||
acc.push(matchingItem.bookFileId);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onDeletePress = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmDelete = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
this.props.onDeletePress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
onConfirmDeleteModalClose = () => {
|
||||
this.setState({ isConfirmDeleteModalOpen: false });
|
||||
}
|
||||
|
||||
onQualityChange = ({ value }) => {
|
||||
const selectedIds = this.getSelectedIds();
|
||||
|
||||
if (!selectedIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onQualityChange(selectedIds, parseInt(value));
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isDeleting,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
qualities
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmDeleteModalOpen
|
||||
} = this.state;
|
||||
|
||||
const qualityOptions = _.reduceRight(qualities, (acc, quality) => {
|
||||
acc.push({
|
||||
key: quality.id,
|
||||
value: quality.name
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, [{ key: 'selectQuality', value: 'Select Quality', disabled: true }]);
|
||||
|
||||
const hasSelectedFiles = this.getSelectedIds().length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{
|
||||
isFetching && !isPopulated ?
|
||||
<LoadingIndicator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error ?
|
||||
<div>{error}</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length ?
|
||||
<div>
|
||||
No book files to manage.
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && items.length ?
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<BookFileEditorRow
|
||||
key={item.id}
|
||||
isSelected={selectedState[item.id]}
|
||||
{...item}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table> :
|
||||
null
|
||||
}
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerButton
|
||||
kind={kinds.DANGER}
|
||||
isSpinning={isDeleting}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onPress={this.onDeletePress}
|
||||
>
|
||||
Delete
|
||||
</SpinnerButton>
|
||||
|
||||
<div className={styles.selectInput}>
|
||||
<SelectInput
|
||||
name="quality"
|
||||
value="selectQuality"
|
||||
values={qualityOptions}
|
||||
isDisabled={!hasSelectedFiles}
|
||||
onChange={this.onQualityChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmDeleteModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Delete Selected Book Files"
|
||||
message={'Are you sure you want to delete the selected book files?'}
|
||||
confirmLabel="Delete"
|
||||
onConfirm={this.onConfirmDelete}
|
||||
onCancel={this.onConfirmDeleteModalClose}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookFileEditorTableContent.propTypes = {
|
||||
isDeleting: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onDeletePress: PropTypes.func.isRequired,
|
||||
onQualityChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default BookFileEditorTableContent;
|
||||
@@ -0,0 +1,125 @@
|
||||
/* eslint max-params: 0 */
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import getQualities from 'Utilities/Quality/getQualities';
|
||||
import createAuthorSelector from 'Store/Selectors/createAuthorSelector';
|
||||
import { deleteBookFiles, updateBookFiles } from 'Store/Actions/bookFileActions';
|
||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||
import BookFileEditorTableContent from './BookFileEditorTableContent';
|
||||
|
||||
function createSchemaSelector() {
|
||||
return createSelector(
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const qualities = getQualities(qualityProfiles.schema.items);
|
||||
|
||||
let error = null;
|
||||
|
||||
if (qualityProfiles.schemaError) {
|
||||
error = 'Unable to load qualities';
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching: qualityProfiles.isSchemaFetching,
|
||||
isPopulated: qualityProfiles.isSchemaPopulated,
|
||||
error,
|
||||
qualities
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { bookId }) => bookId,
|
||||
(state) => state.bookFiles,
|
||||
createSchemaSelector(),
|
||||
createAuthorSelector(),
|
||||
(
|
||||
bookId,
|
||||
bookFiles,
|
||||
schema,
|
||||
author
|
||||
) => {
|
||||
return {
|
||||
...schema,
|
||||
items: bookFiles.items,
|
||||
authorType: author.authorType,
|
||||
isDeleting: bookFiles.isDeleting,
|
||||
isSaving: bookFiles.isSaving
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchQualityProfileSchema(name, path) {
|
||||
dispatch(fetchQualityProfileSchema());
|
||||
},
|
||||
|
||||
dispatchUpdateBookFiles(updateProps) {
|
||||
dispatch(updateBookFiles(updateProps));
|
||||
},
|
||||
|
||||
onDeletePress(bookFileIds) {
|
||||
dispatch(deleteBookFiles({ bookFileIds }));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class BookFileEditorTableContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchQualityProfileSchema();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onQualityChange = (bookFileIds, qualityId) => {
|
||||
const quality = {
|
||||
quality: _.find(this.props.qualities, { id: qualityId }),
|
||||
revision: {
|
||||
version: 1,
|
||||
real: 0
|
||||
}
|
||||
};
|
||||
|
||||
this.props.dispatchUpdateBookFiles({ bookFileIds, quality });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchFetchQualityProfileSchema,
|
||||
dispatchUpdateBookFiles,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<BookFileEditorTableContent
|
||||
{...otherProps}
|
||||
onQualityChange={this.onQualityChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BookFileEditorTableContentConnector.propTypes = {
|
||||
authorId: PropTypes.number.isRequired,
|
||||
bookId: PropTypes.number,
|
||||
qualities: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
dispatchFetchQualityProfileSchema: PropTypes.func.isRequired,
|
||||
dispatchUpdateBookFiles: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, createMapDispatchToProps)(BookFileEditorTableContentConnector);
|
||||
@@ -0,0 +1,61 @@
|
||||
.fileDetails {
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid $borderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.filename {
|
||||
flex-grow: 1;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
font-size: 14px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.expandButton {
|
||||
position: relative;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
composes: button from '~Components/Link/IconButton.css';
|
||||
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.expandButtonIcon {
|
||||
composes: actionButton;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -12px;
|
||||
margin-left: -15px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.medium {
|
||||
border-right: 0;
|
||||
border-left: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.expandButtonIcon {
|
||||
position: static;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import FileDetails from './FileDetails';
|
||||
import styles from './ExpandingFileDetails.css';
|
||||
|
||||
class ExpandingFileDetails extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isExpanded: props.isExpanded
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onExpandPress = () => {
|
||||
const {
|
||||
isExpanded
|
||||
} = this.state;
|
||||
this.setState({ isExpanded: !isExpanded });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
filename,
|
||||
audioTags,
|
||||
rejections
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isExpanded
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.fileDetails}
|
||||
>
|
||||
<div className={styles.header} onClick={this.onExpandPress}>
|
||||
<div className={styles.filename}>
|
||||
{filename}
|
||||
</div>
|
||||
|
||||
<div className={styles.expandButton}>
|
||||
<Icon
|
||||
className={styles.expandButtonIcon}
|
||||
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
||||
title={isExpanded ? 'Hide file info' : 'Show file info'}
|
||||
size={24}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{
|
||||
isExpanded &&
|
||||
<FileDetails
|
||||
audioTags={audioTags}
|
||||
rejections={rejections}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExpandingFileDetails.propTypes = {
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
filename: PropTypes.string.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object),
|
||||
isExpanded: PropTypes.bool
|
||||
};
|
||||
|
||||
export default ExpandingFileDetails;
|
||||
@@ -0,0 +1,11 @@
|
||||
.audioTags {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
/* border-top: 1px solid $borderColor; */
|
||||
}
|
||||
|
||||
.filename {
|
||||
composes: description from '~Components/DescriptionList/DescriptionListItemDescription.css';
|
||||
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Fragment } from 'react';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import Link from 'Components/Link/Link';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import styles from './FileDetails.css';
|
||||
|
||||
function renderRejections(rejections) {
|
||||
return (
|
||||
<span>
|
||||
<DescriptionListItemTitle>
|
||||
Rejections
|
||||
</DescriptionListItemTitle>
|
||||
{
|
||||
_.map(rejections, (item, key) => {
|
||||
return (
|
||||
<DescriptionListItemDescription key={key}>
|
||||
{item.reason}
|
||||
</DescriptionListItemDescription>
|
||||
);
|
||||
})
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function FileDetails(props) {
|
||||
|
||||
const {
|
||||
filename,
|
||||
audioTags,
|
||||
rejections
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles.audioTags}>
|
||||
<DescriptionList>
|
||||
{
|
||||
filename &&
|
||||
<DescriptionListItem
|
||||
title="Filename"
|
||||
data={filename}
|
||||
descriptionClassName={styles.filename}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.title !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Track Title"
|
||||
data={audioTags.title}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.trackNumbers[0] > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Track Number"
|
||||
data={audioTags.trackNumbers[0]}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.discNumber > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Disc Number"
|
||||
data={audioTags.discNumber}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.discCount > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Disc Count"
|
||||
data={audioTags.discCount}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.bookTitle !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Book"
|
||||
data={audioTags.bookTitle}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.authorTitle !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Author"
|
||||
data={audioTags.authorTitle}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.country !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Country"
|
||||
data={audioTags.country.name}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.year > 0 &&
|
||||
<DescriptionListItem
|
||||
title="Year"
|
||||
data={audioTags.year}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.label !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Label"
|
||||
data={audioTags.label}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.catalogNumber !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Catalog Number"
|
||||
data={audioTags.catalogNumber}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.disambiguation !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Disambiguation"
|
||||
data={audioTags.disambiguation}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.duration !== undefined &&
|
||||
<DescriptionListItem
|
||||
title="Duration"
|
||||
data={formatTimeSpan(audioTags.duration)}
|
||||
/>
|
||||
}
|
||||
{
|
||||
audioTags.authorMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/author/${audioTags.authorMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Author ID"
|
||||
data={audioTags.authorMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.bookMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/release-group/${audioTags.bookMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Book ID"
|
||||
data={audioTags.bookMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.releaseMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/release/${audioTags.releaseMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Release ID"
|
||||
data={audioTags.releaseMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.recordingMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/recording/${audioTags.recordingMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Recording ID"
|
||||
data={audioTags.recordingMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
audioTags.trackMBId !== undefined &&
|
||||
<Link
|
||||
to={`https://musicbrainz.org/track/${audioTags.trackMBId}`}
|
||||
>
|
||||
<DescriptionListItem
|
||||
title="MusicBrainz Track ID"
|
||||
data={audioTags.trackMBId}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
{
|
||||
!!rejections && rejections.length > 0 &&
|
||||
renderRejections(rejections)
|
||||
}
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
FileDetails.propTypes = {
|
||||
filename: PropTypes.string,
|
||||
audioTags: PropTypes.object.isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.object)
|
||||
};
|
||||
|
||||
export default FileDetails;
|
||||
@@ -0,0 +1,77 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import { fetchBookFiles } from 'Store/Actions/bookFileActions';
|
||||
import FileDetails from './FileDetails';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.bookFiles,
|
||||
(bookFiles) => {
|
||||
return {
|
||||
...bookFiles
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchBookFiles
|
||||
};
|
||||
|
||||
class FileDetailsConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchBookFiles({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
items,
|
||||
id,
|
||||
isFetching,
|
||||
error
|
||||
} = this.props;
|
||||
|
||||
const item = _.find(items, { id });
|
||||
const errorMessage = getErrorMessage(error, 'Unable to load manual import items');
|
||||
|
||||
if (isFetching || !item.audioTags) {
|
||||
return (
|
||||
<LoadingIndicator />
|
||||
);
|
||||
} else if (error) {
|
||||
return (
|
||||
<div>{errorMessage}</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FileDetails
|
||||
audioTags={item.audioTags}
|
||||
filename={item.path}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
FileDetailsConnector.propTypes = {
|
||||
fetchBookFiles: PropTypes.func.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
id: PropTypes.number.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FileDetailsConnector);
|
||||
@@ -0,0 +1,52 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import FileDetailsConnector from './FileDetailsConnector';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
|
||||
function FileDetailsModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
id
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalContent
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Details
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<FileDetailsConnector
|
||||
id={id}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
FileDetailsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
id: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default FileDetailsModal;
|
||||
@@ -0,0 +1,78 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as mediaInfoTypes from './mediaInfoTypes';
|
||||
|
||||
function MediaInfo(props) {
|
||||
const {
|
||||
type,
|
||||
audioChannels,
|
||||
audioCodec,
|
||||
audioBitRate,
|
||||
audioBits,
|
||||
audioSampleRate
|
||||
} = props;
|
||||
|
||||
if (type === mediaInfoTypes.AUDIO) {
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
!!audioCodec &&
|
||||
audioCodec
|
||||
}
|
||||
|
||||
{
|
||||
!!audioCodec && !!audioChannels &&
|
||||
' - '
|
||||
}
|
||||
|
||||
{
|
||||
!!audioChannels &&
|
||||
audioChannels.toFixed(1)
|
||||
}
|
||||
|
||||
{
|
||||
((!!audioCodec && !!audioBitRate) || (!!audioChannels && !!audioBitRate)) &&
|
||||
' - '
|
||||
}
|
||||
|
||||
{
|
||||
!!audioBitRate &&
|
||||
audioBitRate
|
||||
}
|
||||
|
||||
{
|
||||
((!!audioCodec && !!audioSampleRate) || (!!audioChannels && !!audioSampleRate) || (!!audioBitRate && !!audioSampleRate)) &&
|
||||
' - '
|
||||
}
|
||||
|
||||
{
|
||||
!!audioSampleRate &&
|
||||
audioSampleRate
|
||||
}
|
||||
|
||||
{
|
||||
((!!audioCodec && !!audioBits) || (!!audioChannels && !!audioBits) || (!!audioBitRate && !!audioBits) || (!!audioSampleRate && !!audioBits)) &&
|
||||
' - '
|
||||
}
|
||||
|
||||
{
|
||||
!!audioBits &&
|
||||
audioBits
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
MediaInfo.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
audioChannels: PropTypes.number,
|
||||
audioCodec: PropTypes.string,
|
||||
audioBitRate: PropTypes.string,
|
||||
audioBits: PropTypes.string,
|
||||
audioSampleRate: PropTypes.string
|
||||
};
|
||||
|
||||
export default MediaInfo;
|
||||
@@ -0,0 +1,21 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createBookFileSelector from 'Store/Selectors/createBookFileSelector';
|
||||
import MediaInfo from './MediaInfo';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createBookFileSelector(),
|
||||
(bookFile) => {
|
||||
if (bookFile) {
|
||||
return {
|
||||
...bookFile.mediaInfo
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(MediaInfo);
|
||||
@@ -0,0 +1,2 @@
|
||||
export const AUDIO = 'audio';
|
||||
export const VIDEO = 'video';
|
||||
Reference in New Issue
Block a user