Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 4.7 KiB |
Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 475 B |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 38 KiB |
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 3.2 KiB |
@ -1,210 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { icons, kinds, sizes } from 'Helpers/Props';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import Label from 'Components/Label';
|
|
||||||
import Link from 'Components/Link/Link';
|
|
||||||
import Table from 'Components/Table/Table';
|
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import TrackRowConnector from './TrackRowConnector';
|
|
||||||
import styles from './AlbumDetailsMedium.css';
|
|
||||||
|
|
||||||
function getMediumStatistics(tracks) {
|
|
||||||
let trackCount = 0;
|
|
||||||
let trackFileCount = 0;
|
|
||||||
let totalTrackCount = 0;
|
|
||||||
|
|
||||||
tracks.forEach((track) => {
|
|
||||||
if (track.trackFileId) {
|
|
||||||
trackCount++;
|
|
||||||
trackFileCount++;
|
|
||||||
} else {
|
|
||||||
trackCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalTrackCount++;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
trackCount,
|
|
||||||
trackFileCount,
|
|
||||||
totalTrackCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTrackCountKind(monitored, trackFileCount, trackCount) {
|
|
||||||
if (trackFileCount === trackCount && trackCount > 0) {
|
|
||||||
return kinds.SUCCESS;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!monitored) {
|
|
||||||
return kinds.WARNING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return kinds.DANGER;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AlbumDetailsMedium extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this._expandByDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (prevProps.albumId !== this.props.albumId) {
|
|
||||||
this._expandByDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Control
|
|
||||||
|
|
||||||
_expandByDefault() {
|
|
||||||
const {
|
|
||||||
mediumNumber,
|
|
||||||
onExpandPress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onExpandPress(mediumNumber, mediumNumber === 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onExpandPress = () => {
|
|
||||||
const {
|
|
||||||
mediumNumber,
|
|
||||||
isExpanded
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.onExpandPress(mediumNumber, !isExpanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
mediumNumber,
|
|
||||||
mediumFormat,
|
|
||||||
albumMonitored,
|
|
||||||
items,
|
|
||||||
columns,
|
|
||||||
onTableOptionChange,
|
|
||||||
isExpanded,
|
|
||||||
isSmallScreen
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
trackCount,
|
|
||||||
trackFileCount,
|
|
||||||
totalTrackCount
|
|
||||||
} = getMediumStatistics(items);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={styles.medium}
|
|
||||||
>
|
|
||||||
<div className={styles.header}>
|
|
||||||
<div className={styles.left}>
|
|
||||||
{
|
|
||||||
<div>
|
|
||||||
<span className={styles.mediumNumber}>
|
|
||||||
{mediumFormat} {mediumNumber}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Label
|
|
||||||
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
|
|
||||||
kind={getTrackCountKind(albumMonitored, trackFileCount, trackCount)}
|
|
||||||
size={sizes.LARGE}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
<span>{trackFileCount} / {trackCount}</span>
|
|
||||||
}
|
|
||||||
</Label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
className={styles.expandButton}
|
|
||||||
onPress={this.onExpandPress}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
className={styles.expandButtonIcon}
|
|
||||||
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
|
||||||
title={isExpanded ? 'Hide tracks' : 'Show tracks'}
|
|
||||||
size={24}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
!isSmallScreen &&
|
|
||||||
<span> </span>
|
|
||||||
}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
isExpanded &&
|
|
||||||
<div className={styles.tracks}>
|
|
||||||
{
|
|
||||||
items.length ?
|
|
||||||
<Table
|
|
||||||
columns={columns}
|
|
||||||
onTableOptionChange={onTableOptionChange}
|
|
||||||
>
|
|
||||||
<TableBody>
|
|
||||||
{
|
|
||||||
items.map((item) => {
|
|
||||||
return (
|
|
||||||
<TrackRowConnector
|
|
||||||
key={item.id}
|
|
||||||
columns={columns}
|
|
||||||
{...item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table> :
|
|
||||||
|
|
||||||
<div className={styles.noTracks}>
|
|
||||||
No tracks in this medium
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<div className={styles.collapseButtonContainer}>
|
|
||||||
<IconButton
|
|
||||||
name={icons.COLLAPSE}
|
|
||||||
size={20}
|
|
||||||
title="Hide tracks"
|
|
||||||
onPress={this.onExpandPress}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumDetailsMedium.propTypes = {
|
|
||||||
albumId: PropTypes.number.isRequired,
|
|
||||||
albumMonitored: PropTypes.bool.isRequired,
|
|
||||||
mediumNumber: PropTypes.number.isRequired,
|
|
||||||
mediumFormat: PropTypes.string.isRequired,
|
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
isSaving: PropTypes.bool,
|
|
||||||
isExpanded: PropTypes.bool,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
|
||||||
onExpandPress: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AlbumDetailsMedium;
|
|
@ -1,65 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
|
||||||
import { setTracksTableOption } from 'Store/Actions/trackActions';
|
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
|
||||||
import AlbumDetailsMedium from './AlbumDetailsMedium';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { mediumNumber }) => mediumNumber,
|
|
||||||
(state) => state.tracks,
|
|
||||||
createDimensionsSelector(),
|
|
||||||
(mediumNumber, tracks, dimensions) => {
|
|
||||||
|
|
||||||
const tracksInMedium = _.filter(tracks.items, { mediumNumber });
|
|
||||||
const sortedTracks = _.orderBy(tracksInMedium, ['absoluteTrackNumber'], ['asc']);
|
|
||||||
|
|
||||||
return {
|
|
||||||
items: sortedTracks,
|
|
||||||
columns: tracks.columns,
|
|
||||||
isSmallScreen: dimensions.isSmallScreen
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
setTracksTableOption,
|
|
||||||
executeCommand
|
|
||||||
};
|
|
||||||
|
|
||||||
class AlbumDetailsMediumConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onTableOptionChange = (payload) => {
|
|
||||||
this.props.setTracksTableOption(payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<AlbumDetailsMedium
|
|
||||||
{...this.props}
|
|
||||||
onTableOptionChange={this.onTableOptionChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlbumDetailsMediumConnector.propTypes = {
|
|
||||||
albumId: PropTypes.number.isRequired,
|
|
||||||
albumMonitored: PropTypes.bool.isRequired,
|
|
||||||
mediumNumber: PropTypes.number.isRequired,
|
|
||||||
setTracksTableOption: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsMediumConnector);
|
|
@ -1,6 +0,0 @@
|
|||||||
.TrackActionsCell {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 70px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
|
||||||
import IconButton from 'Components/Link/IconButton';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|
||||||
import FileDetailsModal from 'TrackFile/FileDetailsModal';
|
|
||||||
import styles from './TrackActionsCell.css';
|
|
||||||
|
|
||||||
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() {
|
|
||||||
|
|
||||||
const {
|
|
||||||
trackFileId,
|
|
||||||
trackFilePath
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
isDetailsModalOpen,
|
|
||||||
isConfirmDeleteModalOpen
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRowCell className={styles.TrackActionsCell}>
|
|
||||||
{
|
|
||||||
trackFilePath &&
|
|
||||||
<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>
|
|
||||||
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackActionsCell.propTypes = {
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
albumId: PropTypes.number.isRequired,
|
|
||||||
trackFilePath: PropTypes.string,
|
|
||||||
trackFileId: PropTypes.number.isRequired,
|
|
||||||
deleteTrackFile: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TrackActionsCell;
|
|
@ -1,30 +0,0 @@
|
|||||||
.title {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monitored {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 42px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trackNumber {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 250px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.duration,
|
|
||||||
.status {
|
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
|
||||||
|
|
||||||
width: 100px;
|
|
||||||
}
|
|
@ -1,166 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import TableRow from 'Components/Table/TableRow';
|
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
|
||||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
|
||||||
import EpisodeStatusConnector from 'Album/EpisodeStatusConnector';
|
|
||||||
import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
|
|
||||||
import TrackActionsCell from './TrackActionsCell';
|
|
||||||
import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
|
|
||||||
|
|
||||||
import styles from './TrackRow.css';
|
|
||||||
|
|
||||||
class TrackRow extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
albumId,
|
|
||||||
mediumNumber,
|
|
||||||
trackFileId,
|
|
||||||
absoluteTrackNumber,
|
|
||||||
title,
|
|
||||||
duration,
|
|
||||||
trackFilePath,
|
|
||||||
columns,
|
|
||||||
deleteTrackFile
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TableRow>
|
|
||||||
{
|
|
||||||
columns.map((column) => {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
isVisible
|
|
||||||
} = column;
|
|
||||||
|
|
||||||
if (!isVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'medium') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.trackNumber}
|
|
||||||
>
|
|
||||||
{mediumNumber}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'absoluteTrackNumber') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.trackNumber}
|
|
||||||
>
|
|
||||||
{absoluteTrackNumber}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'title') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.title}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'path') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
{
|
|
||||||
trackFilePath
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'duration') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.duration}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
formatTimeSpan(duration)
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'audioInfo') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.audio}
|
|
||||||
>
|
|
||||||
<MediaInfoConnector
|
|
||||||
type={mediaInfoTypes.AUDIO}
|
|
||||||
trackFileId={trackFileId}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'status') {
|
|
||||||
return (
|
|
||||||
<TableRowCell
|
|
||||||
key={name}
|
|
||||||
className={styles.status}
|
|
||||||
>
|
|
||||||
<EpisodeStatusConnector
|
|
||||||
albumId={id}
|
|
||||||
trackFileId={trackFileId}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name === 'actions') {
|
|
||||||
return (
|
|
||||||
<TrackActionsCell
|
|
||||||
key={name}
|
|
||||||
albumId={albumId}
|
|
||||||
id={id}
|
|
||||||
trackFilePath={trackFilePath}
|
|
||||||
trackFileId={trackFileId}
|
|
||||||
deleteTrackFile={deleteTrackFile}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TrackRow.propTypes = {
|
|
||||||
deleteTrackFile: PropTypes.func.isRequired,
|
|
||||||
id: PropTypes.number.isRequired,
|
|
||||||
albumId: PropTypes.number.isRequired,
|
|
||||||
trackFileId: PropTypes.number,
|
|
||||||
mediumNumber: PropTypes.number.isRequired,
|
|
||||||
trackNumber: PropTypes.string.isRequired,
|
|
||||||
absoluteTrackNumber: PropTypes.number,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
duration: PropTypes.number.isRequired,
|
|
||||||
isSaving: PropTypes.bool,
|
|
||||||
trackFilePath: PropTypes.string,
|
|
||||||
mediaInfo: PropTypes.object,
|
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TrackRow;
|
|
@ -1,23 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector';
|
|
||||||
import { deleteTrackFile } from 'Store/Actions/trackFileActions';
|
|
||||||
import TrackRow from './TrackRow';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state, { id }) => id,
|
|
||||||
createTrackFileSelector(),
|
|
||||||
(id, trackFile) => {
|
|
||||||
return {
|
|
||||||
trackFilePath: trackFile ? trackFile.path : null
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
deleteTrackFile
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(TrackRow);
|
|
@ -1,25 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import EditAlbumModalContentConnector from './EditAlbumModalContentConnector';
|
|
||||||
|
|
||||||
function EditAlbumModal({ isOpen, onModalClose, ...otherProps }) {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<EditAlbumModalContentConnector
|
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditAlbumModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditAlbumModal;
|
|
@ -1,39 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
|
||||||
import EditAlbumModal from './EditAlbumModal';
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
clearPendingChanges
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditAlbumModalConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onModalClose = () => {
|
|
||||||
this.props.clearPendingChanges({ section: 'albums' });
|
|
||||||
this.props.onModalClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditAlbumModal
|
|
||||||
{...this.props}
|
|
||||||
onModalClose={this.onModalClose}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditAlbumModalConnector.propTypes = {
|
|
||||||
onModalClose: PropTypes.func.isRequired,
|
|
||||||
clearPendingChanges: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(undefined, mapDispatchToProps)(EditAlbumModalConnector);
|
|
@ -1,133 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { inputTypes } from 'Helpers/Props';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
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 Form from 'Components/Form/Form';
|
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
|
||||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
|
||||||
|
|
||||||
class EditAlbumModalContent extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
const {
|
|
||||||
onSavePress
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
onSavePress(false);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
title,
|
|
||||||
artistName,
|
|
||||||
albumType,
|
|
||||||
statistics,
|
|
||||||
item,
|
|
||||||
isSaving,
|
|
||||||
onInputChange,
|
|
||||||
onModalClose,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
monitored,
|
|
||||||
anyReleaseOk,
|
|
||||||
releases
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
Edit - {artistName} - {title} [{albumType}]
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<Form
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>Monitored</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="monitored"
|
|
||||||
helpText="Readarr will search for and download album"
|
|
||||||
{...monitored}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel>Automatically Switch Release</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.CHECK}
|
|
||||||
name="anyReleaseOk"
|
|
||||||
helpText="Readarr will automatically switch to the release best matching downloaded tracks"
|
|
||||||
{...anyReleaseOk}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<FormGroup>
|
|
||||||
<FormLabel> Release</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
|
||||||
type={inputTypes.ALBUM_RELEASE_SELECT}
|
|
||||||
name="releases"
|
|
||||||
helpText="Change release for this album"
|
|
||||||
isDisabled={anyReleaseOk.value && statistics.trackFileCount > 0}
|
|
||||||
albumReleases={releases}
|
|
||||||
onChange={onInputChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
</Form>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
|
||||||
onPress={onModalClose}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<SpinnerButton
|
|
||||||
isSpinning={isSaving}
|
|
||||||
onPress={this.onSavePress}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</SpinnerButton>
|
|
||||||
</ModalFooter>
|
|
||||||
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditAlbumModalContent.propTypes = {
|
|
||||||
albumId: PropTypes.number.isRequired,
|
|
||||||
title: PropTypes.string.isRequired,
|
|
||||||
artistName: PropTypes.string.isRequired,
|
|
||||||
albumType: PropTypes.string.isRequired,
|
|
||||||
statistics: PropTypes.object.isRequired,
|
|
||||||
item: PropTypes.object.isRequired,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
onInputChange: PropTypes.func.isRequired,
|
|
||||||
onSavePress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default EditAlbumModalContent;
|
|
@ -1,98 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import selectSettings from 'Store/Selectors/selectSettings';
|
|
||||||
import createAlbumSelector from 'Store/Selectors/createAlbumSelector';
|
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
|
||||||
import { setAlbumValue, saveAlbum } from 'Store/Actions/albumActions';
|
|
||||||
import EditAlbumModalContent from './EditAlbumModalContent';
|
|
||||||
|
|
||||||
function createMapStateToProps() {
|
|
||||||
return createSelector(
|
|
||||||
(state) => state.albums,
|
|
||||||
createAlbumSelector(),
|
|
||||||
createArtistSelector(),
|
|
||||||
(albumState, album, artist) => {
|
|
||||||
const {
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
pendingChanges
|
|
||||||
} = albumState;
|
|
||||||
|
|
||||||
const albumSettings = _.pick(album, [
|
|
||||||
'monitored',
|
|
||||||
'anyReleaseOk',
|
|
||||||
'releases'
|
|
||||||
]);
|
|
||||||
|
|
||||||
const settings = selectSettings(albumSettings, pendingChanges, saveError);
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: album.title,
|
|
||||||
artistName: artist.artistName,
|
|
||||||
albumType: album.albumType,
|
|
||||||
statistics: album.statistics,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
item: settings.settings,
|
|
||||||
...settings
|
|
||||||
};
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
dispatchSetAlbumValue: setAlbumValue,
|
|
||||||
dispatchSaveAlbum: saveAlbum
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditAlbumModalContentConnector extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
|
||||||
if (prevProps.isSaving && !this.props.isSaving && !this.props.saveError) {
|
|
||||||
this.props.onModalClose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onInputChange = ({ name, value }) => {
|
|
||||||
this.props.dispatchSetAlbumValue({ name, value });
|
|
||||||
}
|
|
||||||
|
|
||||||
onSavePress = () => {
|
|
||||||
this.props.dispatchSaveAlbum({
|
|
||||||
id: this.props.albumId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<EditAlbumModalContent
|
|
||||||
{...this.props}
|
|
||||||
onInputChange={this.onInputChange}
|
|
||||||
onSavePress={this.onSavePress}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EditAlbumModalContentConnector.propTypes = {
|
|
||||||
albumId: PropTypes.number,
|
|
||||||
isSaving: PropTypes.bool.isRequired,
|
|
||||||
saveError: PropTypes.object,
|
|
||||||
dispatchSetAlbumValue: PropTypes.func.isRequired,
|
|
||||||
dispatchSaveAlbum: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EditAlbumModalContentConnector);
|
|
@ -1,160 +0,0 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React, { Component } from 'react';
|
|
||||||
import LazyLoad from 'react-lazyload';
|
|
||||||
|
|
||||||
const logoPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII=';
|
|
||||||
|
|
||||||
function findLogo(images) {
|
|
||||||
return _.find(images, { coverType: 'logo' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLogoUrl(logo, size) {
|
|
||||||
if (logo) {
|
|
||||||
// Remove protocol
|
|
||||||
let url = logo.url.replace(/^https?:/, '');
|
|
||||||
url = url.replace('logo.jpg', `logo-${size}.jpg`);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistLogo extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const pixelRatio = Math.floor(window.devicePixelRatio);
|
|
||||||
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const logo = findLogo(images);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
pixelRatio,
|
|
||||||
logo,
|
|
||||||
logoUrl: getLogoUrl(logo, pixelRatio * size),
|
|
||||||
hasError: false,
|
|
||||||
isLoaded: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
pixelRatio
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const logo = findLogo(images);
|
|
||||||
|
|
||||||
if (logo && logo.url !== this.state.logo.url) {
|
|
||||||
this.setState({
|
|
||||||
logo,
|
|
||||||
logoUrl: getLogoUrl(logo, pixelRatio * size),
|
|
||||||
hasError: false,
|
|
||||||
isLoaded: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onError = () => {
|
|
||||||
this.setState({ hasError: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoad = () => {
|
|
||||||
this.setState({ isLoaded: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
size,
|
|
||||||
lazy,
|
|
||||||
overflow
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
logoUrl,
|
|
||||||
hasError,
|
|
||||||
isLoaded
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (hasError || !logoUrl) {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={logoPlaceholder}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lazy) {
|
|
||||||
return (
|
|
||||||
<LazyLoad
|
|
||||||
height={size}
|
|
||||||
offset={100}
|
|
||||||
overflow={overflow}
|
|
||||||
placeholder={
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={logoPlaceholder}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={logoUrl}
|
|
||||||
onError={this.onError}
|
|
||||||
/>
|
|
||||||
</LazyLoad>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
className={className}
|
|
||||||
style={style}
|
|
||||||
src={isLoaded ? logoUrl : logoPlaceholder}
|
|
||||||
onError={this.onError}
|
|
||||||
onLoad={this.onLoad}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ArtistLogo.propTypes = {
|
|
||||||
className: PropTypes.string,
|
|
||||||
style: PropTypes.object,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
lazy: PropTypes.bool.isRequired,
|
|
||||||
overflow: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
ArtistLogo.defaultProps = {
|
|
||||||
size: 250,
|
|
||||||
lazy: true,
|
|
||||||
overflow: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArtistLogo;
|
|
@ -0,0 +1,205 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import getToggledRange from 'Utilities/Table/getToggledRange';
|
||||||
|
import { icons, sortDirections } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import AlbumRowConnector from './AlbumRowConnector';
|
||||||
|
import styles from './AuthorDetailsSeries.css';
|
||||||
|
|
||||||
|
class AuthorDetailsSeries extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
isOrganizeModalOpen: false,
|
||||||
|
isManageTracksOpen: false,
|
||||||
|
lastToggledAlbum: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this._expandByDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps) {
|
||||||
|
const {
|
||||||
|
authorId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.authorId !== authorId) {
|
||||||
|
this._expandByDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
_expandByDefault() {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
onExpandPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onExpandPress(id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onExpandPress = () => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
isExpanded
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
this.props.onExpandPress(id, !isExpanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMonitorAlbumPress = (albumId, monitored, { shiftKey }) => {
|
||||||
|
const lastToggled = this.state.lastToggledAlbum;
|
||||||
|
const albumIds = [albumId];
|
||||||
|
|
||||||
|
if (shiftKey && lastToggled) {
|
||||||
|
const { lower, upper } = getToggledRange(this.props.items, albumId, lastToggled);
|
||||||
|
const items = this.props.items;
|
||||||
|
|
||||||
|
for (let i = lower; i < upper; i++) {
|
||||||
|
albumIds.push(items[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ lastToggledAlbum: albumId });
|
||||||
|
|
||||||
|
this.props.onMonitorAlbumPress(_.uniq(albumIds), monitored);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
positionMap,
|
||||||
|
columns,
|
||||||
|
isExpanded,
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
onSortPress,
|
||||||
|
isSmallScreen,
|
||||||
|
onTableOptionChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.albumType}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className={styles.expandButton}
|
||||||
|
onPress={this.onExpandPress}
|
||||||
|
>
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div className={styles.left}>
|
||||||
|
{
|
||||||
|
<div>
|
||||||
|
<span className={styles.albumTypeLabel}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className={styles.albumCount}>
|
||||||
|
({items.length} Books)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Icon
|
||||||
|
className={styles.expandButtonIcon}
|
||||||
|
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
||||||
|
title={isExpanded ? 'Hide books' : 'Show books'}
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
!isSmallScreen &&
|
||||||
|
<span> </span>
|
||||||
|
}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
isExpanded &&
|
||||||
|
<div className={styles.albums}>
|
||||||
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
sortKey={sortKey}
|
||||||
|
sortDirection={sortDirection}
|
||||||
|
onSortPress={onSortPress}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<AlbumRowConnector
|
||||||
|
key={item.id}
|
||||||
|
columns={columns}
|
||||||
|
{...item}
|
||||||
|
position={positionMap[item.id]}
|
||||||
|
onMonitorAlbumPress={this.onMonitorAlbumPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
<div className={styles.collapseButtonContainer}>
|
||||||
|
<IconButton
|
||||||
|
iconClassName={styles.collapseButtonIcon}
|
||||||
|
name={icons.COLLAPSE}
|
||||||
|
size={20}
|
||||||
|
title="Hide books"
|
||||||
|
onPress={this.onExpandPress}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuthorDetailsSeries.propTypes = {
|
||||||
|
id: PropTypes.number.isRequired,
|
||||||
|
authorId: PropTypes.number.isRequired,
|
||||||
|
label: PropTypes.string.isRequired,
|
||||||
|
sortKey: PropTypes.string,
|
||||||
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
positionMap: PropTypes.object.isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
isExpanded: PropTypes.bool,
|
||||||
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
onExpandPress: PropTypes.func.isRequired,
|
||||||
|
onSortPress: PropTypes.func.isRequired,
|
||||||
|
onMonitorAlbumPress: PropTypes.func.isRequired,
|
||||||
|
uiSettings: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AuthorDetailsSeries;
|
@ -0,0 +1,121 @@
|
|||||||
|
/* 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 createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
// import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import { toggleAlbumsMonitored, setAlbumsTableOption } from 'Store/Actions/albumActions';
|
||||||
|
import { setSeriesSort } from 'Store/Actions/seriesActions';
|
||||||
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
import AuthorDetailsSeries from './AuthorDetailsSeries';
|
||||||
|
|
||||||
|
function createMapStateToProps() {
|
||||||
|
return createSelector(
|
||||||
|
(state, { seriesId }) => seriesId,
|
||||||
|
(state) => state.albums,
|
||||||
|
createArtistSelector(),
|
||||||
|
(state) => state.series,
|
||||||
|
createCommandsSelector(),
|
||||||
|
createDimensionsSelector(),
|
||||||
|
createUISettingsSelector(),
|
||||||
|
(seriesId, books, author, series, commands, dimensions, uiSettings) => {
|
||||||
|
|
||||||
|
const currentSeries = _.find(series.items, { id: seriesId });
|
||||||
|
|
||||||
|
const bookIds = currentSeries.links.map((x) => x.bookId);
|
||||||
|
const positionMap = currentSeries.links.reduce((acc, curr) => {
|
||||||
|
acc[curr.bookId] = curr.position;
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
const booksInSeries = _.filter(books.items, (book) => bookIds.includes(book.id));
|
||||||
|
|
||||||
|
let sortDir = 'asc';
|
||||||
|
|
||||||
|
if (series.sortDirection === 'descending') {
|
||||||
|
sortDir = 'desc';
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortedBooks = [];
|
||||||
|
if (series.sortKey === 'position') {
|
||||||
|
sortedBooks = booksInSeries.sort((a, b) => {
|
||||||
|
const apos = positionMap[a.id] || '';
|
||||||
|
const bpos = positionMap[b.id] || '';
|
||||||
|
return apos.localeCompare(bpos, undefined, { numeric: true, sensivity: 'base' });
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
sortedBooks = _.orderBy(booksInSeries, series.sortKey, sortDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: currentSeries.id,
|
||||||
|
label: currentSeries.title,
|
||||||
|
items: sortedBooks,
|
||||||
|
positionMap,
|
||||||
|
columns: series.columns,
|
||||||
|
sortKey: series.sortKey,
|
||||||
|
sortDirection: series.sortDirection,
|
||||||
|
artistMonitored: author.monitored,
|
||||||
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
uiSettings
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
toggleAlbumsMonitored,
|
||||||
|
setAlbumsTableOption,
|
||||||
|
dispatchSetSeriesSort: setSeriesSort,
|
||||||
|
executeCommand
|
||||||
|
};
|
||||||
|
|
||||||
|
class ArtistDetailsSeasonConnector extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setAlbumsTableOption(payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSortPress = (sortKey) => {
|
||||||
|
this.props.dispatchSetSeriesSort({ sortKey });
|
||||||
|
}
|
||||||
|
|
||||||
|
onMonitorAlbumPress = (bookIds, monitored) => {
|
||||||
|
this.props.toggleAlbumsMonitored({
|
||||||
|
bookIds,
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<AuthorDetailsSeries
|
||||||
|
{...this.props}
|
||||||
|
onSortPress={this.onSortPress}
|
||||||
|
onTableOptionChange={this.onTableOptionChange}
|
||||||
|
onMonitorAlbumPress={this.onMonitorAlbumPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistDetailsSeasonConnector.propTypes = {
|
||||||
|
authorId: PropTypes.number.isRequired,
|
||||||
|
toggleAlbumsMonitored: PropTypes.func.isRequired,
|
||||||
|
setAlbumsTableOption: PropTypes.func.isRequired,
|
||||||
|
dispatchSetSeriesSort: PropTypes.func.isRequired,
|
||||||
|
executeCommand: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsSeasonConnector);
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ArtistHistoryContentConnector from 'Artist/History/ArtistHistoryContentConnector';
|
||||||
|
import ArtistHistoryTableContent from 'Artist/History/ArtistHistoryTableContent';
|
||||||
|
|
||||||
|
function ArtistHistoryTable(props) {
|
||||||
|
const {
|
||||||
|
...otherProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ArtistHistoryContentConnector
|
||||||
|
component={ArtistHistoryTableContent}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistHistoryTable.propTypes = {
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistHistoryTable;
|
@ -0,0 +1,113 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import Table from 'Components/Table/Table';
|
||||||
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import ArtistHistoryRowConnector from './ArtistHistoryRowConnector';
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
name: 'eventType',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'album',
|
||||||
|
label: 'Album',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sourceTitle',
|
||||||
|
label: 'Source Title',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: 'Quality',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'date',
|
||||||
|
label: 'Date',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'details',
|
||||||
|
label: 'Details',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'actions',
|
||||||
|
label: 'Actions',
|
||||||
|
isVisible: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
class ArtistHistoryTableContent extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
bookId,
|
||||||
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
error,
|
||||||
|
items,
|
||||||
|
onMarkAsFailedPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const fullArtist = bookId == null;
|
||||||
|
const hasItems = !!items.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to load history.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && !hasItems && !error &&
|
||||||
|
<div>No history.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isPopulated && hasItems && !error &&
|
||||||
|
<Table columns={columns}>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
items.map((item) => {
|
||||||
|
return (
|
||||||
|
<ArtistHistoryRowConnector
|
||||||
|
key={item.id}
|
||||||
|
fullArtist={fullArtist}
|
||||||
|
{...item}
|
||||||
|
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtistHistoryTableContent.propTypes = {
|
||||||
|
bookId: PropTypes.number,
|
||||||
|
isFetching: PropTypes.bool.isRequired,
|
||||||
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
|
error: PropTypes.object,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArtistHistoryTableContent;
|
@ -1,33 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
|
||||||
import ArtistInteractiveSearchModalContent from './ArtistInteractiveSearchModalContent';
|
|
||||||
|
|
||||||
function ArtistInteractiveSearchModal(props) {
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
artistId,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
closeOnBackgroundClick={false}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
|
||||||
<ArtistInteractiveSearchModalContent
|
|
||||||
artistId={artistId}
|
|
||||||
onModalClose={onModalClose}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArtistInteractiveSearchModal.propTypes = {
|
|
||||||
isOpen: PropTypes.bool.isRequired,
|
|
||||||
artistId: PropTypes.number.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArtistInteractiveSearchModal;
|
|
@ -1,15 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
|
||||||
import ArtistInteractiveSearchModal from './ArtistInteractiveSearchModal';
|
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
|
||||||
return {
|
|
||||||
onModalClose() {
|
|
||||||
dispatch(cancelFetchReleases());
|
|
||||||
dispatch(clearReleases());
|
|
||||||
props.onModalClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default connect(null, createMapDispatchToProps)(ArtistInteractiveSearchModal);
|
|
@ -1,45 +0,0 @@
|
|||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'Components/Link/Button';
|
|
||||||
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 InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
|
|
||||||
|
|
||||||
function ArtistInteractiveSearchModalContent(props) {
|
|
||||||
const {
|
|
||||||
artistId,
|
|
||||||
onModalClose
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ModalContent onModalClose={onModalClose}>
|
|
||||||
<ModalHeader>
|
|
||||||
Interactive Search
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
|
||||||
<InteractiveSearchConnector
|
|
||||||
type="artist"
|
|
||||||
searchPayload={{
|
|
||||||
artistId
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ModalBody>
|
|
||||||
|
|
||||||
<ModalFooter>
|
|
||||||
<Button onPress={onModalClose}>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</ModalContent>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArtistInteractiveSearchModalContent.propTypes = {
|
|
||||||
artistId: PropTypes.number.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArtistInteractiveSearchModalContent;
|
|