New: Unmapped files view (#888)
* New: Unmapped files view Displays all trackfiles that haven't been matched to a track. Generalised the file details component and adds it to the album details screen. * Add sorting by quality * New: MediaServiceTests & MediaRepoTestspull/6/head
parent
74cb2a6f52
commit
4413c7e46c
@ -1,34 +1,109 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import FileDetailsModal from 'TrackFile/FileDetailsModal';
|
||||||
import styles from './TrackActionsCell.css';
|
import styles from './TrackActionsCell.css';
|
||||||
|
|
||||||
class TrackActionsCell extends Component {
|
class TrackActionsCell extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isDetailsModalOpen: false,
|
||||||
|
isConfirmDeleteModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDetailsPress = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDetailsModalClose = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteFilePress = () => {
|
||||||
|
this.setState({ isConfirmDeleteModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDelete = () => {
|
||||||
|
this.setState({ isConfirmDeleteModalOpen: false });
|
||||||
|
this.props.deleteTrackFile({ id: this.props.trackFileId });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDeleteModalClose = () => {
|
||||||
|
this.setState({ isConfirmDeleteModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
||||||
return (
|
const {
|
||||||
|
trackFileId,
|
||||||
|
trackFilePath
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
// TODO: Placeholder until we figure out what to show here.
|
const {
|
||||||
|
isDetailsModalOpen,
|
||||||
|
isConfirmDeleteModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
<TableRowCell className={styles.TrackActionsCell}>
|
<TableRowCell className={styles.TrackActionsCell}>
|
||||||
<IconButton
|
{
|
||||||
name={icons.DELETE}
|
trackFilePath &&
|
||||||
title="Delete Track"
|
<IconButton
|
||||||
|
name={icons.INFO}
|
||||||
|
onPress={this.onDetailsPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
trackFilePath &&
|
||||||
|
<IconButton
|
||||||
|
name={icons.DELETE}
|
||||||
|
onPress={this.onDeleteFilePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<FileDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
onModalClose={this.onDetailsModalClose}
|
||||||
|
id={trackFileId}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete Track File"
|
||||||
|
message={`Are you sure you want to delete ${trackFilePath}?`}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={this.onConfirmDelete}
|
||||||
|
onCancel={this.onConfirmDeleteModalClose}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackActionsCell.propTypes = {
|
TrackActionsCell.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
albumId: PropTypes.number.isRequired
|
albumId: PropTypes.number.isRequired,
|
||||||
|
trackFilePath: PropTypes.string,
|
||||||
|
trackFileId: PropTypes.number.isRequired,
|
||||||
|
deleteTrackFile: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TrackActionsCell;
|
export default TrackActionsCell;
|
||||||
|
@ -1,258 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { icons } from 'Helpers/Props';
|
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
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';
|
|
||||||
|
|
||||||
class FileDetails 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
|
|
||||||
|
|
||||||
renderRejections() {
|
|
||||||
const {
|
|
||||||
rejections
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span>
|
|
||||||
<DescriptionListItemTitle>
|
|
||||||
Rejections
|
|
||||||
</DescriptionListItemTitle>
|
|
||||||
{
|
|
||||||
_.map(rejections, (item, key) => {
|
|
||||||
return (
|
|
||||||
<DescriptionListItemDescription key={key}>
|
|
||||||
{item.reason}
|
|
||||||
</DescriptionListItemDescription>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isExpanded &&
|
|
||||||
<div className={styles.audioTags}>
|
|
||||||
|
|
||||||
<DescriptionList>
|
|
||||||
{
|
|
||||||
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.albumTitle !== undefined &&
|
|
||||||
<DescriptionListItem
|
|
||||||
title="Album"
|
|
||||||
data={audioTags.albumTitle}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
audioTags.artistTitle !== undefined &&
|
|
||||||
<DescriptionListItem
|
|
||||||
title="Artist"
|
|
||||||
data={audioTags.artistTitle}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
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.artistMBId !== undefined &&
|
|
||||||
<Link
|
|
||||||
to={`https://musicbrainz.org/artist/${audioTags.artistMBId}`}
|
|
||||||
>
|
|
||||||
<DescriptionListItem
|
|
||||||
title="MusicBrainz Artist ID"
|
|
||||||
data={audioTags.artistMBId}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
audioTags.albumMBId !== undefined &&
|
|
||||||
<Link
|
|
||||||
to={`https://musicbrainz.org/release-group/${audioTags.albumMBId}`}
|
|
||||||
>
|
|
||||||
<DescriptionListItem
|
|
||||||
title="MusicBrainz Album ID"
|
|
||||||
data={audioTags.albumMBId}
|
|
||||||
/>
|
|
||||||
</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.length > 0 &&
|
|
||||||
this.renderRejections()
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FileDetails.propTypes = {
|
|
||||||
audioTags: PropTypes.object.isRequired,
|
|
||||||
filename: PropTypes.string.isRequired,
|
|
||||||
rejections: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isExpanded: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FileDetails;
|
|
@ -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.albumTitle !== undefined &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Album"
|
||||||
|
data={audioTags.albumTitle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
audioTags.artistTitle !== undefined &&
|
||||||
|
<DescriptionListItem
|
||||||
|
title="Artist"
|
||||||
|
data={audioTags.artistTitle}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
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.artistMBId !== undefined &&
|
||||||
|
<Link
|
||||||
|
to={`https://musicbrainz.org/artist/${audioTags.artistMBId}`}
|
||||||
|
>
|
||||||
|
<DescriptionListItem
|
||||||
|
title="MusicBrainz Artist ID"
|
||||||
|
data={audioTags.artistMBId}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
audioTags.albumMBId !== undefined &&
|
||||||
|
<Link
|
||||||
|
to={`https://musicbrainz.org/release-group/${audioTags.albumMBId}`}
|
||||||
|
>
|
||||||
|
<DescriptionListItem
|
||||||
|
title="MusicBrainz Album ID"
|
||||||
|
data={audioTags.albumMBId}
|
||||||
|
/>
|
||||||
|
</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 { fetchTrackFiles } from 'Store/Actions/trackFileActions';
|
||||||
|
import FileDetails from './FileDetails';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state) => state.trackFiles,
|
||||||
|
(trackFiles) => {
|
||||||
|
return {
|
||||||
|
...trackFiles
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
fetchTrackFiles
|
||||||
|
};
|
||||||
|
|
||||||
|
class FileDetailsConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.fetchTrackFiles({ 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 = {
|
||||||
|
fetchTrackFiles: 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,161 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { align, icons, sortDirections } from 'Helpers/Props';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import VirtualTable from 'Components/Table/VirtualTable';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
|
import UnmappedFilesTableRow from './UnmappedFilesTableRow';
|
||||||
|
import UnmappedFilesTableHeader from './UnmappedFilesTableHeader';
|
||||||
|
|
||||||
|
class UnmappedFilesTable extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
contentBody: null,
|
||||||
|
scrollTop: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
setContentBodyRef = (ref) => {
|
||||||
|
this.setState({ contentBody: ref });
|
||||||
|
}
|
||||||
|
|
||||||
|
rowRenderer = ({ key, rowIndex, style }) => {
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
deleteUnmappedFile
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const item = items[rowIndex];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnmappedFilesTableRow
|
||||||
|
style={style}
|
||||||
|
key={key}
|
||||||
|
columns={columns}
|
||||||
|
deleteUnmappedFile={deleteUnmappedFile}
|
||||||
|
{...item}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onScroll = ({ scrollTop }) => {
|
||||||
|
this.setState({ scrollTop });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
columns,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
onTableOptionChange,
|
||||||
|
onSortPress,
|
||||||
|
deleteUnmappedFile,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
scrollTop,
|
||||||
|
contentBody
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent title="UnmappedFiles">
|
||||||
|
<PageToolbar>
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Options"
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
|
</PageToolbarSection>
|
||||||
|
</PageToolbar>
|
||||||
|
|
||||||
|
<PageContentBodyConnector
|
||||||
|
ref={this.setContentBodyRef}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
isFetching && !isPopulated &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !items.length &&
|
||||||
|
<div>
|
||||||
|
Success! My work is done, all files on disk are matched to known tracks.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !error && !!items.length && contentBody &&
|
||||||
|
<VirtualTable
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
contentBody={contentBody}
|
||||||
|
isSmallScreen={false}
|
||||||
|
scrollTop={scrollTop}
|
||||||
|
onScroll={this.onScroll}
|
||||||
|
overscanRowCount={10}
|
||||||
|
rowRenderer={this.rowRenderer}
|
||||||
|
header={
|
||||||
|
<UnmappedFilesTableHeader
|
||||||
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
</PageContentBodyConnector>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnmappedFilesTable.propTypes = {
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
deleteUnmappedFile: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnmappedFilesTable;
|
@ -0,0 +1,100 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import { fetchTrackFiles, deleteTrackFile, setTrackFilesSort, setTrackFilesTableOption } from 'Store/Actions/trackFileActions';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
|
import UnmappedFilesTable from './UnmappedFilesTable';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
createClientSideCollectionSelector('trackFiles'),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
(
|
||||||
|
trackFiles,
|
||||||
|
dimensionsState
|
||||||
|
) => {
|
||||||
|
// trackFiles could pick up mapped entries via signalR so filter again here
|
||||||
|
const {
|
||||||
|
items,
|
||||||
|
...otherProps
|
||||||
|
} = trackFiles;
|
||||||
|
const unmappedFiles = _.filter(items, { albumId: 0 });
|
||||||
|
return {
|
||||||
|
items: unmappedFiles,
|
||||||
|
...otherProps,
|
||||||
|
isSmallScreen: dimensionsState.isSmallScreen
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
|
return {
|
||||||
|
onTableOptionChange(payload) {
|
||||||
|
dispatch(setTrackFilesTableOption(payload));
|
||||||
|
},
|
||||||
|
|
||||||
|
onSortPress(sortKey) {
|
||||||
|
dispatch(setTrackFilesSort({ sortKey }));
|
||||||
|
},
|
||||||
|
|
||||||
|
fetchUnmappedFiles() {
|
||||||
|
dispatch(fetchTrackFiles({ unmapped: true }));
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteUnmappedFile(id) {
|
||||||
|
dispatch(deleteTrackFile({ id }));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnmappedFilesTableConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
registerPagePopulator(this.repopulate, ['trackFileUpdated']);
|
||||||
|
|
||||||
|
this.repopulate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
unregisterPagePopulator(this.repopulate);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
repopulate = () => {
|
||||||
|
this.props.fetchUnmappedFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<UnmappedFilesTable
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UnmappedFilesTableConnector.propTypes = {
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
fetchUnmappedFiles: PropTypes.func.isRequired,
|
||||||
|
deleteUnmappedFile: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, createMapDispatchToProps)(UnmappedFilesTableConnector)
|
||||||
|
);
|
@ -0,0 +1,19 @@
|
|||||||
|
.quality,
|
||||||
|
.size,
|
||||||
|
.dateAdded {
|
||||||
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 0 0 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.path {
|
||||||
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 4 0 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
flex: 0 1 90px;
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
|
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
|
// import hasGrowableColumns from './hasGrowableColumns';
|
||||||
|
import styles from './UnmappedFilesTableHeader.css';
|
||||||
|
|
||||||
|
function UnmappedFilesTableHeader(props) {
|
||||||
|
const {
|
||||||
|
columns,
|
||||||
|
onTableOptionChange,
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableHeader>
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
isSortable,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
name={name}
|
||||||
|
isSortable={false}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
name={icons.ADVANCED_SETTINGS}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableHeaderCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
name={name}
|
||||||
|
isSortable={isSortable}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</VirtualTableHeaderCell>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</VirtualTableHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UnmappedFilesTableHeader.propTypes = {
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onTableOptionChange: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnmappedFilesTableHeader;
|
@ -0,0 +1,22 @@
|
|||||||
|
.path {
|
||||||
|
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 4 0 400px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: $monoSpaceFontFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality,
|
||||||
|
.dateAdded,
|
||||||
|
.size {
|
||||||
|
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 0 0 120px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
composes: cell from '~Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
flex: 0 0 90px;
|
||||||
|
}
|
@ -0,0 +1,218 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
|
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||||
|
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
|
import FileDetailsModal from 'TrackFile/FileDetailsModal';
|
||||||
|
import styles from './UnmappedFilesTableRow.css';
|
||||||
|
|
||||||
|
class UnmappedFilesTableRow extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isDetailsModalOpen: false,
|
||||||
|
isInteractiveImportModalOpen: false,
|
||||||
|
isConfirmDeleteModalOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onDetailsPress = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDetailsModalClose = () => {
|
||||||
|
this.setState({ isDetailsModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onInteractiveImportPress = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onInteractiveImportModalClose = () => {
|
||||||
|
this.setState({ isInteractiveImportModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteFilePress = () => {
|
||||||
|
this.setState({ isConfirmDeleteModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDelete = () => {
|
||||||
|
this.setState({ isConfirmDeleteModalOpen: false });
|
||||||
|
this.props.deleteUnmappedFile(this.props.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
onConfirmDeleteModalClose = () => {
|
||||||
|
this.setState({ isConfirmDeleteModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
style,
|
||||||
|
id,
|
||||||
|
path,
|
||||||
|
size,
|
||||||
|
dateAdded,
|
||||||
|
quality,
|
||||||
|
columns
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const folder = path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')));
|
||||||
|
|
||||||
|
const {
|
||||||
|
isInteractiveImportModalOpen,
|
||||||
|
isDetailsModalOpen,
|
||||||
|
isConfirmDeleteModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtualTableRow
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
columns.map((column) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
isVisible
|
||||||
|
} = column;
|
||||||
|
|
||||||
|
if (!isVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'path') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
{path}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'size') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
{formatBytes(size)}
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'dateAdded') {
|
||||||
|
return (
|
||||||
|
<RelativeDateCellConnector
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
date={dateAdded}
|
||||||
|
component={VirtualTableRowCell}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'quality') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
<TrackQuality
|
||||||
|
quality={quality}
|
||||||
|
/>
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'actions') {
|
||||||
|
return (
|
||||||
|
<VirtualTableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles[name]}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
name={icons.INFO}
|
||||||
|
onPress={this.onDetailsPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.INTERACTIVE}
|
||||||
|
onPress={this.onInteractiveImportPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name={icons.DELETE}
|
||||||
|
onPress={this.onDeleteFilePress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</VirtualTableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
<InteractiveImportModal
|
||||||
|
isOpen={isInteractiveImportModalOpen}
|
||||||
|
folder={folder}
|
||||||
|
showFilterExistingFiles={true}
|
||||||
|
filterExistingFiles={false}
|
||||||
|
showImportMode={false}
|
||||||
|
showReplaceExistingFiles={false}
|
||||||
|
replaceExistingFiles={false}
|
||||||
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FileDetailsModal
|
||||||
|
isOpen={isDetailsModalOpen}
|
||||||
|
onModalClose={this.onDetailsModalClose}
|
||||||
|
id={id}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmDeleteModalOpen}
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
title="Delete Track File"
|
||||||
|
message={`Are you sure you want to delete ${path}?`}
|
||||||
|
confirmLabel="Delete"
|
||||||
|
onConfirm={this.onConfirmDelete}
|
||||||
|
onCancel={this.onConfirmDeleteModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
</VirtualTableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
UnmappedFilesTableRow.propTypes = {
|
||||||
|
style: PropTypes.object.isRequired,
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
path: PropTypes.string.isRequired,
|
||||||
|
size: PropTypes.number.isRequired,
|
||||||
|
quality: PropTypes.object.isRequired,
|
||||||
|
dateAdded: PropTypes.string.isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
deleteUnmappedFile: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnmappedFilesTableRow;
|
Loading…
Reference in new issue