[UI Work] Artist Detail, Album Dialog, Album Search, Album Missing Search

pull/84/head
Qstick 7 years ago
parent 5fec72395c
commit 0a7f18e843

@ -10,13 +10,13 @@ class AlbumStudioAlbum extends Component {
//
// Listeners
onSeasonMonitoredPress = () => {
onAlbumMonitoredPress = () => {
const {
id,
monitored
} = this.props;
this.props.onSeasonMonitoredPress(id, !monitored);
this.props.onAlbumMonitoredPress(id, !monitored);
}
//
@ -43,7 +43,7 @@ class AlbumStudioAlbum extends Component {
<MonitorToggleButton
monitored={monitored}
isSaving={isSaving}
onPress={this.onSeasonMonitoredPress}
onPress={this.onAlbumMonitoredPress}
/>
<span>
@ -75,7 +75,7 @@ AlbumStudioAlbum.propTypes = {
monitored: PropTypes.bool.isRequired,
statistics: PropTypes.object.isRequired,
isSaving: PropTypes.bool.isRequired,
onSeasonMonitoredPress: PropTypes.func.isRequired
onAlbumMonitoredPress: PropTypes.func.isRequired
};
AlbumStudioAlbum.defaultProps = {

@ -26,8 +26,8 @@ class AlbumStudioRow extends Component {
isSaving,
isSelected,
onSelectedChange,
onSeriesMonitoredPress,
onSeasonMonitoredPress
onArtistMonitoredPress,
onAlbumMonitoredPress
} = this.props;
return (
@ -41,7 +41,7 @@ class AlbumStudioRow extends Component {
<TableRowCell className={styles.status}>
<Icon
className={styles.statusIcon}
name={status === 'ended' ? icons.SERIES_ENDED : icons.SERIES_CONTINUING}
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
title={status === 'ended' ? 'Ended' : 'Continuing'}
/>
@ -58,7 +58,7 @@ class AlbumStudioRow extends Component {
<MonitorToggleButton
monitored={monitored}
isSaving={isSaving}
onPress={onSeriesMonitoredPress}
onPress={onArtistMonitoredPress}
/>
</TableRowCell>
@ -69,7 +69,7 @@ class AlbumStudioRow extends Component {
<AlbumStudioAlbum
key={season.id}
{...season}
onSeasonMonitoredPress={onSeasonMonitoredPress}
onAlbumMonitoredPress={onAlbumMonitoredPress}
/>
);
})
@ -90,8 +90,8 @@ AlbumStudioRow.propTypes = {
isSaving: PropTypes.bool.isRequired,
isSelected: PropTypes.bool,
onSelectedChange: PropTypes.func.isRequired,
onSeriesMonitoredPress: PropTypes.func.isRequired,
onSeasonMonitoredPress: PropTypes.func.isRequired
onArtistMonitoredPress: PropTypes.func.isRequired,
onAlbumMonitoredPress: PropTypes.func.isRequired
};
AlbumStudioRow.defaultProps = {

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { toggleSeriesMonitored, toggleSeasonMonitored } from 'Store/Actions/artistActions';
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
import AlbumStudioRow from './AlbumStudioRow';
function createMapStateToProps() {
@ -25,7 +26,8 @@ function createMapStateToProps() {
const mapDispatchToProps = {
toggleSeriesMonitored,
toggleSeasonMonitored
toggleSeasonMonitored,
toggleEpisodeMonitored
};
class AlbumStudioRowConnector extends Component {
@ -33,7 +35,7 @@ class AlbumStudioRowConnector extends Component {
//
// Listeners
onSeriesMonitoredPress = () => {
onArtistMonitoredPress = () => {
const {
artistId,
monitored
@ -45,11 +47,10 @@ class AlbumStudioRowConnector extends Component {
});
}
onSeasonMonitoredPress = (seasonNumber, monitored) => {
this.props.toggleSeasonMonitored({
artistId: this.props.artistId,
seasonNumber,
monitored
onAlbumMonitoredPress = (episodeId, monitored) => {
this.props.toggleEpisodeMonitored({
episodeId,
monitored: !monitored
});
}
@ -60,8 +61,8 @@ class AlbumStudioRowConnector extends Component {
return (
<AlbumStudioRow
{...this.props}
onSeriesMonitoredPress={this.onSeriesMonitoredPress}
onSeasonMonitoredPress={this.onSeasonMonitoredPress}
onArtistMonitoredPress={this.onArtistMonitoredPress}
onAlbumMonitoredPress={this.onAlbumMonitoredPress}
/>
);
}
@ -71,7 +72,8 @@ AlbumStudioRowConnector.propTypes = {
artistId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
toggleSeriesMonitored: PropTypes.func.isRequired,
toggleSeasonMonitored: PropTypes.func.isRequired
toggleSeasonMonitored: PropTypes.func.isRequired,
toggleEpisodeMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector);

@ -13,7 +13,7 @@ import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector'
import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
import SeriesDetailsPageConnector from 'Artist/Details/SeriesDetailsPageConnector';
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
@ -93,7 +93,7 @@ function App({ store, history }) {
<Route
path="/artist/:nameSlug"
component={SeriesDetailsPageConnector}
component={ArtistDetailsPageConnector}
/>
{/*

@ -10,15 +10,6 @@
width: 42px;
}
.episodeNumber {
composes: cell from 'Components/Table/Cells/TableRowCell.css';
width: 50px;
}
.language,
.audio,
.video,
.status {
composes: cell from 'Components/Table/Cells/TableRowCell.css';

@ -0,0 +1,219 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import { kinds, sizes } from 'Helpers/Props';
import TableRow from 'Components/Table/TableRow';
import Label from 'Components/Label';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import styles from './AlbumRow.css';
function getEpisodeCountKind(monitored, episodeFileCount, episodeCount) {
if (episodeFileCount === episodeCount && episodeCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
class AlbumRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
//
// Listeners
onManualSearchPress = () => {
this.setState({ isDetailsModalOpen: true });
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
}
onMonitorAlbumPress = (monitored, options) => {
this.props.onMonitorAlbumPress(this.props.id, monitored, options);
}
//
// Render
render() {
const {
id,
artistId,
monitored,
statistics,
duration,
releaseDate,
title,
isSaving,
artistMonitored,
path,
columns
} = this.props;
const {
trackCount,
trackFileCount,
totalTrackCount
} = statistics;
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'monitored') {
return (
<TableRowCell
key={name}
className={styles.monitored}
>
<MonitorToggleButton
monitored={monitored}
isDisabled={!artistMonitored}
isSaving={isSaving}
onPress={this.onMonitorAlbumPress}
/>
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell
key={name}
className={styles.title}
>
<EpisodeTitleLink
episodeId={id}
artistId={artistId}
episodeTitle={title}
showOpenSeriesButton={false}
/>
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{
path
}
</TableRowCell>
);
}
if (name === 'trackCount') {
return (
<TableRowCell key={name}>
{
statistics.trackCount
}
</TableRowCell>
);
}
if (name === 'duration') {
return (
<TableRowCell key={name}>
{
formatTimeSpan(duration)
}
</TableRowCell>
);
}
if (name === 'releaseDate') {
return (
<RelativeDateCellConnector
key={name}
date={releaseDate}
/>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<Label
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
kind={getEpisodeCountKind(monitored, trackFileCount, trackCount)}
size={sizes.MEDIUM}
>
{
<span>{trackFileCount} / {trackCount}</span>
}
</Label>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<EpisodeSearchCellConnector
key={name}
episodeId={id}
artistId={artistId}
episodeTitle={title}
/>
);
}
return null;
})
}
</TableRow>
);
}
}
AlbumRow.propTypes = {
id: PropTypes.number.isRequired,
artistId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
releaseDate: PropTypes.string.isRequired,
duration: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
unverifiedSceneNumbering: PropTypes.bool,
artistMonitored: PropTypes.bool.isRequired,
statistics: PropTypes.object.isRequired,
path: PropTypes.string,
mediaInfo: PropTypes.object,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onMonitorAlbumPress: PropTypes.func.isRequired
};
export default AlbumRow;

@ -4,7 +4,7 @@ import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createEpisodeFileSelector from 'Store/Selectors/createEpisodeFileSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import EpisodeRow from './EpisodeRow';
import AlbumRow from './AlbumRow';
function createMapStateToProps() {
return createSelector(
@ -17,7 +17,7 @@ function createMapStateToProps() {
const alternateTitles = sceneSeasonNumber ? _.filter(series.alternateTitles, { sceneSeasonNumber }) : [];
return {
seriesMonitored: series.monitored,
artistMonitored: series.monitored,
seriesType: series.seriesType,
episodeFilePath: episodeFile ? episodeFile.path : null,
episodeFileRelativePath: episodeFile ? episodeFile.relativePath : null,
@ -26,4 +26,4 @@ function createMapStateToProps() {
}
);
}
export default connect(createMapStateToProps)(EpisodeRow);
export default connect(createMapStateToProps)(AlbumRow);

@ -1,8 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import styles from './SeriesAlternateTitles.css';
import styles from './ArtistAlternateTitles.css';
function SeriesAlternateTitles({ alternateTitles }) {
function ArtistAlternateTitles({ alternateTitles }) {
return (
<ul>
{
@ -21,8 +21,8 @@ function SeriesAlternateTitles({ alternateTitles }) {
);
}
SeriesAlternateTitles.propTypes = {
ArtistAlternateTitles.propTypes = {
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default SeriesAlternateTitles;
export default ArtistAlternateTitles;

@ -61,11 +61,11 @@
line-height: 50px;
}
.seriesNavigationButtons {
.artistNavigationButtons {
white-space: no-wrap;
}
.seriesNavigationButton {
.artistNavigationButton {
composes: button from 'Components/Link/IconButton.css';
margin-left: 5px;
@ -90,7 +90,6 @@
.sizeOnDisk,
.qualityProfileName,
.network,
.links,
.tags {
margin-left: 8px;

@ -24,11 +24,29 @@ import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfil
import ArtistPoster from 'Artist/ArtistPoster';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
import SeriesAlternateTitles from './SeriesAlternateTitles';
import SeriesDetailsSeasonConnector from './SeriesDetailsSeasonConnector';
import SeriesTagsConnector from './SeriesTagsConnector';
import SeriesDetailsLinks from './SeriesDetailsLinks';
import styles from './SeriesDetails.css';
import ArtistAlternateTitles from './ArtistAlternateTitles';
import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
import ArtistTagsConnector from './ArtistTagsConnector';
import ArtistDetailsLinks from './ArtistDetailsLinks';
import styles from './ArtistDetails.css';
const albumTypes = [
{
name: 'album',
label: 'Album',
isVisible: true
},
{
name: 'single',
label: 'Single',
isVisible: true
},
{
name: 'ep',
label: 'EP',
isVisible: true
}
];
function getFanartUrl(images) {
const fanartImage = _.find(images, { coverType: 'fanart' });
@ -46,7 +64,7 @@ function getExpandedState(newState) {
};
}
class SeriesDetails extends Component {
class ArtistDetails extends Component {
//
// Lifecycle
@ -133,8 +151,6 @@ class SeriesDetails extends Component {
const {
id,
foreignArtistId,
tvMazeId,
imdbId,
artistName,
ratings,
sizeOnDisk,
@ -142,7 +158,6 @@ class SeriesDetails extends Component {
qualityProfileId,
monitored,
status,
network,
overview,
images,
albums,
@ -154,8 +169,8 @@ class SeriesDetails extends Component {
isPopulated,
episodesError,
episodeFilesError,
previousSeries,
nextSeries,
previousArtist,
nextArtist,
onRefreshPress,
onSearchPress
} = this.props;
@ -172,12 +187,12 @@ class SeriesDetails extends Component {
const continuing = status === 'continuing';
let episodeFilesCountMessage = 'No episode files';
let episodeFilesCountMessage = 'No track files';
if (trackFileCount === 1) {
episodeFilesCountMessage = '1 episode file';
episodeFilesCountMessage = '1 track file';
} else if (trackFileCount > 1) {
episodeFilesCountMessage = `${trackFileCount} episode files`;
episodeFilesCountMessage = `${trackFileCount} track files`;
}
let expandIcon = icons.EXPAND_INDETERMINATE;
@ -217,7 +232,7 @@ class SeriesDetails extends Component {
/>
<PageToolbarButton
label="Manage Episodes"
label="Manage Tracks"
iconName={icons.EPISODE_FILE}
onPress={this.onManageEpisodesPress}
/>
@ -281,28 +296,28 @@ class SeriesDetails extends Component {
/>
}
title="Alternate Titles"
body={<SeriesAlternateTitles alternateTitles={alternateTitles} />}
body={<ArtistAlternateTitles alternateTitles={alternateTitles} />}
position={tooltipPositions.BOTTOM}
/>
</span>
}
</div>
<div className={styles.seriesNavigationButtons}>
<div className={styles.artistNavigationButtons}>
<IconButton
className={styles.seriesNavigationButton}
className={styles.artistNavigationButton}
name={icons.ARROW_LEFT}
size={30}
title={`Go to ${previousSeries.artistName}`}
to={`/artist/${previousSeries.nameSlug}`}
title={`Go to ${previousArtist.artistName}`}
to={`/artist/${previousArtist.nameSlug}`}
/>
<IconButton
className={styles.seriesNavigationButton}
className={styles.artistNavigationButton}
name={icons.ARROW_RIGHT}
size={30}
title={`Go to ${nextSeries.artistName}`}
to={`/artist/${nextSeries.nameSlug}`}
title={`Go to ${nextArtist.artistName}`}
to={`/artist/${nextArtist.nameSlug}`}
/>
</div>
</div>
@ -369,11 +384,11 @@ class SeriesDetails extends Component {
<Label
className={styles.detailsLabel}
title={continuing ? 'More episodes/another season is expected' : 'No additional episodes or or another season is expected'}
title={continuing ? 'More albums are expected' : 'No additional albums are expected'}
size={sizes.LARGE}
>
<Icon
name={continuing ? icons.SERIES_CONTINUING : icons.SERIES_ENDED}
name={continuing ? icons.ARTIST_CONTINUING : icons.ARTIST_ENDED}
size={17}
/>
@ -382,24 +397,6 @@ class SeriesDetails extends Component {
</span>
</Label>
{
!!network &&
<Label
className={styles.detailsLabel}
title="Network"
size={sizes.LARGE}
>
<Icon
name={icons.NETWORK}
size={17}
/>
<span className={styles.qualityProfileName}>
{network}
</span>
</Label>
}
<Tooltip
anchor={
<Label
@ -417,10 +414,8 @@ class SeriesDetails extends Component {
</Label>
}
tooltip={
<SeriesDetailsLinks
<ArtistDetailsLinks
foreignArtistId={foreignArtistId}
tvMazeId={tvMazeId}
imdbId={imdbId}
/>
}
kind={kinds.INVERSE}
@ -445,7 +440,7 @@ class SeriesDetails extends Component {
</span>
</Label>
}
tooltip={<SeriesTagsConnector artistId={id} />}
tooltip={<ArtistTagsConnector artistId={id} />}
kind={kinds.INVERSE}
position={tooltipPositions.BOTTOM}
/>
@ -477,18 +472,17 @@ class SeriesDetails extends Component {
}
{
isPopulated && !!albums.length &&
isPopulated && !!albumTypes.length &&
<div>
{
albums.slice(0).reverse().map((season) => {
albumTypes.slice(0).map((season) => {
return (
<SeriesDetailsSeasonConnector
key={season.id}
<ArtistDetailsSeasonConnector
key={season.name}
artistId={id}
albumId={season.id}
statistics={season.statistics}
label={season.label}
{...season}
isExpanded={expandedState[season.id]}
isExpanded={expandedState[season.name]}
onExpandPress={this.onExpandPress}
/>
);
@ -536,11 +530,9 @@ class SeriesDetails extends Component {
}
}
SeriesDetails.propTypes = {
ArtistDetails.propTypes = {
id: PropTypes.number.isRequired,
foreignArtistId: PropTypes.string.isRequired,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string,
artistName: PropTypes.string.isRequired,
ratings: PropTypes.object.isRequired,
sizeOnDisk: PropTypes.number.isRequired,
@ -548,7 +540,6 @@ SeriesDetails.propTypes = {
qualityProfileId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
status: PropTypes.string.isRequired,
network: PropTypes.string,
overview: PropTypes.string.isRequired,
images: PropTypes.arrayOf(PropTypes.object).isRequired,
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
@ -560,10 +551,10 @@ SeriesDetails.propTypes = {
isPopulated: PropTypes.bool.isRequired,
episodesError: PropTypes.object,
episodeFilesError: PropTypes.object,
previousSeries: PropTypes.object.isRequired,
nextSeries: PropTypes.object.isRequired,
previousArtist: PropTypes.object.isRequired,
nextArtist: PropTypes.object.isRequired,
onRefreshPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
};
export default SeriesDetails;
export default ArtistDetails;

@ -11,7 +11,7 @@ import { fetchEpisodeFiles, clearEpisodeFiles } from 'Store/Actions/episodeFileA
import { fetchQueueDetails, clearQueueDetails } from 'Store/Actions/queueActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import SeriesDetails from './SeriesDetails';
import ArtistDetails from './ArtistDetails';
function createMapStateToProps() {
return createSelector(
@ -21,7 +21,7 @@ function createMapStateToProps() {
createAllArtistSelector(),
createCommandsSelector(),
(nameSlug, episodes, episodeFiles, allSeries, commands) => {
const sortedArtist = _.orderBy(allSeries, 'sortTitle');
const sortedArtist = _.orderBy(allSeries, 'sortName');
const seriesIndex = _.findIndex(sortedArtist, { nameSlug });
const series = sortedArtist[seriesIndex];
@ -29,15 +29,15 @@ function createMapStateToProps() {
return {};
}
const previousSeries = sortedArtist[seriesIndex - 1] || _.last(sortedArtist);
const nextSeries = sortedArtist[seriesIndex + 1] || _.first(sortedArtist);
const isSeriesRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: series.id });
const allSeriesRefreshing = _.some(commands, (command) => command.name === commandNames.REFRESH_ARTIST && !command.body.artistId);
const isRefreshing = isSeriesRefreshing || allSeriesRefreshing;
const previousArtist = sortedArtist[seriesIndex - 1] || _.last(sortedArtist);
const nextArtist = sortedArtist[seriesIndex + 1] || _.first(sortedArtist);
const isArtistRefreshing = !!findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: series.id });
const allArtistRefreshing = _.some(commands, (command) => command.name === commandNames.REFRESH_ARTIST && !command.body.artistId);
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
const isSearching = !!findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: series.id });
const isRenamingFiles = !!findCommand(commands, { name: commandNames.RENAME_FILES, artistId: series.id });
const isRenamingSeriesCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
const isRenamingSeries = !!(isRenamingSeriesCommand && isRenamingSeriesCommand.body.artistId.indexOf(series.id) > -1);
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
const isRenamingArtist = !!(isRenamingArtistCommand && isRenamingArtistCommand.body.artistId.indexOf(series.id) > -1);
const isFetching = episodes.isFetching || episodeFiles.isFetching;
const isPopulated = episodes.isPopulated && episodeFiles.isPopulated;
@ -58,13 +58,13 @@ function createMapStateToProps() {
isRefreshing,
isSearching,
isRenamingFiles,
isRenamingSeries,
isRenamingArtist,
isFetching,
isPopulated,
episodesError,
episodeFilesError,
previousSeries,
nextSeries
previousArtist,
nextArtist
};
}
);
@ -80,7 +80,7 @@ const mapDispatchToProps = {
executeCommand
};
class SeriesDetailsConnector extends Component {
class ArtistDetailsConnector extends Component {
//
// Lifecycle
@ -94,13 +94,13 @@ class SeriesDetailsConnector extends Component {
id,
isRefreshing,
isRenamingFiles,
isRenamingSeries
isRenamingArtist
} = this.props;
if (
(prevProps.isRefreshing && !isRefreshing) ||
(prevProps.isRenamingFiles && !isRenamingFiles) ||
(prevProps.isRenamingSeries && !isRenamingSeries)
(prevProps.isRenamingArtist && !isRenamingArtist)
) {
this._populate();
}
@ -157,7 +157,7 @@ class SeriesDetailsConnector extends Component {
render() {
return (
<SeriesDetails
<ArtistDetails
{...this.props}
onRefreshPress={this.onRefreshPress}
onSearchPress={this.onSearchPress}
@ -166,12 +166,12 @@ class SeriesDetailsConnector extends Component {
}
}
SeriesDetailsConnector.propTypes = {
ArtistDetailsConnector.propTypes = {
id: PropTypes.number.isRequired,
nameSlug: PropTypes.string.isRequired,
isRefreshing: PropTypes.bool.isRequired,
isRenamingFiles: PropTypes.bool.isRequired,
isRenamingSeries: PropTypes.bool.isRequired,
isRenamingArtist: PropTypes.bool.isRequired,
fetchEpisodes: PropTypes.func.isRequired,
clearEpisodes: PropTypes.func.isRequired,
fetchEpisodeFiles: PropTypes.func.isRequired,
@ -181,4 +181,4 @@ SeriesDetailsConnector.propTypes = {
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SeriesDetailsConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsConnector);

@ -0,0 +1,36 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds, sizes } from 'Helpers/Props';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import styles from './ArtistDetailsLinks.css';
function ArtistDetailsLinks(props) {
const {
foreignArtistId
} = props;
return (
<div className={styles.links}>
<Link
className={styles.link}
to={`https://musicbrainz.org/artist/${foreignArtistId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Musicbrainz
</Label>
</Link>
</div>
);
}
ArtistDetailsLinks.propTypes = {
foreignArtistId: PropTypes.string.isRequired
};
export default ArtistDetailsLinks;

@ -6,7 +6,7 @@ import { createSelector } from 'reselect';
import { push } from 'react-router-redux';
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
import NotFound from 'Components/NotFound';
import SeriesDetailsConnector from './SeriesDetailsConnector';
import ArtistDetailsConnector from './ArtistDetailsConnector';
function createMapStateToProps() {
return createSelector(
@ -31,7 +31,7 @@ const mapDispatchToProps = {
push
};
class SeriesDetailsPageConnector extends Component {
class ArtistDetailsPageConnector extends Component {
//
// Lifecycle
@ -60,17 +60,17 @@ class SeriesDetailsPageConnector extends Component {
}
return (
<SeriesDetailsConnector
<ArtistDetailsConnector
nameSlug={nameSlug}
/>
);
}
}
SeriesDetailsPageConnector.propTypes = {
ArtistDetailsPageConnector.propTypes = {
nameSlug: PropTypes.string,
match: PropTypes.shape({ params: PropTypes.shape({ nameSlug: PropTypes.string.isRequired }).isRequired }).isRequired,
push: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SeriesDetailsPageConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsPageConnector);

@ -1,4 +1,4 @@
.season {
.albumType {
margin-bottom: 20px;
border: 1px solid $borderColor;
border-radius: 4px;
@ -17,11 +17,17 @@
font-size: 24px;
}
.seasonNumber {
margin-right: 10px;
.albumTypeLabel {
margin-right: 5px;
margin-left: 5px;
}
.albumCount {
font-size: 18px;
font-style: italic;
color: #8895aa;
}
.episodeCountContainer {
margin-left: 10px;
vertical-align: text-bottom;
@ -38,7 +44,7 @@
.left {
display: flex;
align-items: center;
flex: 0 1 600px;
flex: 0 1 300px;
}
.left,

@ -7,9 +7,7 @@ import getToggledRange from 'Utilities/Table/getToggledRange';
import { align, 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 MonitorToggleButton from 'Components/MonitorToggleButton';
import SpinnerIcon from 'Components/SpinnerIcon';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import Menu from 'Components/Menu/Menu';
@ -20,22 +18,10 @@ import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import EpisodeFileEditorModal from 'EpisodeFile/Editor/EpisodeFileEditorModal';
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
import EpisodeRowConnector from './EpisodeRowConnector';
import styles from './SeriesDetailsSeason.css';
import AlbumRowConnector from './AlbumRowConnector';
import styles from './ArtistDetailsSeason.css';
function getEpisodeCountKind(monitored, episodeFileCount, episodeCount) {
if (episodeFileCount === episodeCount && episodeCount > 0) {
return kinds.SUCCESS;
}
if (!monitored) {
return kinds.WARNING;
}
return kinds.DANGER;
}
class SeriesDetailsSeason extends Component {
class ArtistDetailsSeason extends Component {
//
// Lifecycle
@ -65,7 +51,7 @@ class SeriesDetailsSeason extends Component {
_expandByDefault() {
const {
albumId,
name,
onExpandPress,
items
} = this.props;
@ -75,7 +61,7 @@ class SeriesDetailsSeason extends Component {
isAfter(item.airDateUtc, { days: -30 });
});
onExpandPress(albumId, expand && albumId > 0);
onExpandPress(name, expand && name > 0);
}
//
@ -99,29 +85,29 @@ class SeriesDetailsSeason extends Component {
onExpandPress = () => {
const {
albumId,
name,
isExpanded
} = this.props;
this.props.onExpandPress(albumId, !isExpanded);
this.props.onExpandPress(name, !isExpanded);
}
onMonitorEpisodePress = (episodeId, monitored, { shiftKey }) => {
onMonitorAlbumPress = (albumId, monitored, { shiftKey }) => {
const lastToggled = this.state.lastToggledEpisode;
const episodeIds = [episodeId];
const albumIds = [albumId];
if (shiftKey && lastToggled) {
const { lower, upper } = getToggledRange(this.props.items, episodeId, lastToggled);
const { lower, upper } = getToggledRange(this.props.items, albumId, lastToggled);
const items = this.props.items;
for (let i = lower; i < upper; i++) {
episodeIds.push(items[i].id);
albumIds.push(items[i].id);
}
}
this.setState({ lastToggledEpisode: episodeId });
this.setState({ lastToggledEpisode: albumId });
this.props.onMonitorEpisodePress(_.uniq(episodeIds), monitored);
this.props.onMonitorAlbumPress(_.uniq(albumIds), monitored);
}
//
@ -130,29 +116,19 @@ class SeriesDetailsSeason extends Component {
render() {
const {
artistId,
monitored,
title,
releaseDate,
albumId,
statistics,
label,
items,
columns,
isSaving,
isExpanded,
isSearching,
seriesMonitored,
artistMonitored,
isSmallScreen,
onTableOptionChange,
onMonitorSeasonPress,
onSearchPress
} = this.props;
const {
trackCount,
trackFileCount,
totalTrackCount
} = statistics;
const {
isOrganizeModalOpen,
isManageEpisodesOpen
@ -160,37 +136,22 @@ class SeriesDetailsSeason extends Component {
return (
<div
className={styles.season}
className={styles.albumType}
>
<div className={styles.header}>
<div className={styles.left}>
<MonitorToggleButton
monitored={monitored}
isDisabled={!seriesMonitored}
isSaving={isSaving}
size={24}
onPress={onMonitorSeasonPress}
/>
{
albumId === 0 ?
<span className={styles.seasonNumber}>
Specials
</span> :
<span className={styles.seasonNumber}>
{title}
<div>
<span className={styles.albumTypeLabel}>
{label}
</span>
<span className={styles.albumCount}>
({items.length} Releases)
</span>
</div>
}
<Label
title={`${totalTrackCount} tracks total. ${trackFileCount} tracks with files.`}
kind={getEpisodeCountKind(monitored, trackFileCount, trackCount)}
size={sizes.LARGE}
>
{
<span>{trackFileCount} / {trackCount}</span>
}
</Label>
</div>
<Link
@ -198,6 +159,13 @@ class SeriesDetailsSeason extends Component {
onPress={this.onExpandPress}
>
<Icon
className={styles.expandButtonIcon}
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
title={isExpanded ? 'Hide albums' : 'Show albums'}
size={24}
/>
{
!isSmallScreen &&
<span>&nbsp;</span>
@ -266,37 +234,62 @@ class SeriesDetailsSeason extends Component {
onPress={onSearchPress}
/>
<IconButton
className={styles.actionButton}
name={icons.ORGANIZE}
title="Preview rename for this album"
size={24}
onPress={this.onOrganizePress}
/>
<IconButton
className={styles.actionButton}
name={icons.EPISODE_FILE}
title="Manage track files in this artist"
size={24}
onPress={this.onManageEpisodesPress}
/>
</div>
}
</div>
<div>
{
isExpanded &&
<div className={styles.episodes}>
{
items.length ?
<Table
columns={columns}
onTableOptionChange={onTableOptionChange}
>
<TableBody>
{
items.map((item) => {
return (
<AlbumRowConnector
key={item.id}
columns={columns}
{...item}
onMonitorAlbumPress={this.onMonitorAlbumPress}
/>
);
})
}
</TableBody>
</Table> :
<div className={styles.noEpisodes}>
No albums in this group
</div>
}
<div className={styles.collapseButtonContainer}>
<IconButton
name={icons.COLLAPSE}
size={20}
title="Hide episodes"
onPress={this.onExpandPress}
/>
</div>
</div>
}
</div>
<OrganizePreviewModalConnector
isOpen={isOrganizeModalOpen}
artistId={artistId}
albumId={albumId}
onModalClose={this.onOrganizeModalClose}
/>
<EpisodeFileEditorModal
isOpen={isManageEpisodesOpen}
artistId={artistId}
albumId={albumId}
onModalClose={this.onManageEpisodesModalClose}
/>
</div>
@ -304,33 +297,22 @@ class SeriesDetailsSeason extends Component {
}
}
SeriesDetailsSeason.propTypes = {
ArtistDetailsSeason.propTypes = {
artistId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
title: PropTypes.string.isRequired,
releaseDate: PropTypes.string.isRequired,
albumId: PropTypes.number.isRequired,
statistics: PropTypes.object.isRequired,
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isSaving: PropTypes.bool,
isExpanded: PropTypes.bool,
isSearching: PropTypes.bool.isRequired,
seriesMonitored: PropTypes.bool.isRequired,
artistMonitored: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onMonitorSeasonPress: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired,
onMonitorEpisodePress: PropTypes.func.isRequired,
onMonitorAlbumPress: PropTypes.func.isRequired,
onSearchPress: PropTypes.func.isRequired
};
SeriesDetailsSeason.defaultProps = {
statistics: {
trackFileCount: 0,
totalTrackCount: 0,
percentOfTracks: 0
}
};
export default SeriesDetailsSeason;
export default ArtistDetailsSeason;

@ -11,30 +11,30 @@ import { toggleSeasonMonitored } from 'Store/Actions/artistActions';
import { toggleEpisodesMonitored, setEpisodesTableOption } from 'Store/Actions/episodeActions';
import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import SeriesDetailsSeason from './SeriesDetailsSeason';
import ArtistDetailsSeason from './ArtistDetailsSeason';
function createMapStateToProps() {
return createSelector(
(state, { seasonNumber }) => seasonNumber,
(state, { label }) => label,
(state) => state.episodes,
createArtistSelector(),
createCommandsSelector(),
createDimensionsSelector(),
(seasonNumber, episodes, series, commands, dimensions) => {
(label, episodes, series, commands, dimensions) => {
const isSearching = !!findCommand(commands, {
name: commandNames.SEASON_SEARCH,
artistId: series.id,
seasonNumber
label
});
const episodesInSeason = _.filter(episodes.items, { seasonNumber });
const sortedEpisodes = _.orderBy(episodesInSeason, 'episodeNumber', 'desc');
const episodesInSeason = _.filter(episodes.items, { albumType: label });
const sortedEpisodes = _.orderBy(episodesInSeason, 'releaseDate', 'desc');
return {
items: sortedEpisodes,
columns: episodes.columns,
isSearching,
seriesMonitored: series.monitored,
artistMonitored: series.monitored,
isSmallScreen: dimensions.isSmallScreen
};
}
@ -48,7 +48,7 @@ const mapDispatchToProps = {
executeCommand
};
class SeriesDetailsSeasonConnector extends Component {
class ArtistDetailsSeasonConnector extends Component {
//
// Listeners
@ -59,33 +59,29 @@ class SeriesDetailsSeasonConnector extends Component {
onMonitorSeasonPress = (monitored) => {
const {
artistId,
albumId
artistId
} = this.props;
this.props.toggleSeasonMonitored({
artistId,
albumId,
monitored
});
}
onSearchPress = () => {
const {
artistId,
albumId
artistId
} = this.props;
this.props.executeCommand({
name: commandNames.SEASON_SEARCH,
artistId,
albumIds: [albumId]
artistId
});
}
onMonitorEpisodePress = (episodeIds, monitored) => {
onMonitorAlbumPress = (albumIds, monitored) => {
this.props.toggleEpisodesMonitored({
episodeIds,
albumIds,
monitored
});
}
@ -95,24 +91,23 @@ class SeriesDetailsSeasonConnector extends Component {
render() {
return (
<SeriesDetailsSeason
<ArtistDetailsSeason
{...this.props}
onTableOptionChange={this.onTableOptionChange}
onMonitorSeasonPress={this.onMonitorSeasonPress}
onSearchPress={this.onSearchPress}
onMonitorEpisodePress={this.onMonitorEpisodePress}
onMonitorAlbumPress={this.onMonitorAlbumPress}
/>
);
}
}
SeriesDetailsSeasonConnector.propTypes = {
ArtistDetailsSeasonConnector.propTypes = {
artistId: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
toggleSeasonMonitored: PropTypes.func.isRequired,
toggleEpisodesMonitored: PropTypes.func.isRequired,
setEpisodesTableOption: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SeriesDetailsSeasonConnector);
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistDetailsSeasonConnector);

@ -2,9 +2,9 @@ import PropTypes from 'prop-types';
import React from 'react';
import { kinds, sizes } from 'Helpers/Props';
import Label from 'Components/Label';
import styles from './SeriesTags.css';
import styles from './ArtistTags.css';
function SeriesTags({ tags }) {
function ArtistTags({ tags }) {
return (
<div>
{
@ -24,8 +24,8 @@ function SeriesTags({ tags }) {
);
}
SeriesTags.propTypes = {
ArtistTags.propTypes = {
tags: PropTypes.arrayOf(PropTypes.string).isRequired
};
export default SeriesTags;
export default ArtistTags;

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createTagsSelector from 'Store/Selectors/createTagsSelector';
import SeriesTags from './SeriesTags';
import ArtistTags from './ArtistTags';
function createMapStateToProps() {
return createSelector(
@ -27,4 +27,4 @@ function createMapStateToProps() {
);
}
export default connect(createMapStateToProps)(SeriesTags);
export default connect(createMapStateToProps)(ArtistTags);

@ -1,266 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import TableRow from 'Components/Table/TableRow';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import EpisodeSearchCellConnector from 'Episode/EpisodeSearchCellConnector';
import EpisodeNumber from 'Episode/EpisodeNumber';
import EpisodeTitleLink from 'Episode/EpisodeTitleLink';
import EpisodeStatusConnector from 'Episode/EpisodeStatusConnector';
import EpisodeFileLanguageConnector from 'EpisodeFile/EpisodeFileLanguageConnector';
import MediaInfoConnector from 'EpisodeFile/MediaInfoConnector';
import * as mediaInfoTypes from 'EpisodeFile/mediaInfoTypes';
import styles from './EpisodeRow.css';
class EpisodeRow extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isDetailsModalOpen: false
};
}
//
// Listeners
onManualSearchPress = () => {
this.setState({ isDetailsModalOpen: true });
}
onDetailsModalClose = () => {
this.setState({ isDetailsModalOpen: false });
}
onMonitorEpisodePress = (monitored, options) => {
this.props.onMonitorEpisodePress(this.props.id, monitored, options);
}
//
// Render
render() {
const {
id,
artistId,
episodeFileId,
monitored,
seasonNumber,
episodeNumber,
absoluteEpisodeNumber,
sceneSeasonNumber,
sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber,
airDateUtc,
title,
unverifiedSceneNumbering,
isSaving,
seriesMonitored,
seriesType,
episodeFilePath,
episodeFileRelativePath,
alternateTitles,
columns
} = this.props;
return (
<TableRow>
{
columns.map((column) => {
const {
name,
isVisible
} = column;
if (!isVisible) {
return null;
}
if (name === 'monitored') {
return (
<TableRowCell
key={name}
className={styles.monitored}
>
<MonitorToggleButton
monitored={monitored}
isDisabled={!seriesMonitored}
isSaving={isSaving}
onPress={this.onMonitorEpisodePress}
/>
</TableRowCell>
);
}
if (name === 'episodeNumber') {
return (
<TableRowCell
key={name}
className={styles.episodeNumber}
>
<EpisodeNumber
seasonNumber={seasonNumber}
episodeNumber={episodeNumber}
absoluteEpisodeNumber={absoluteEpisodeNumber}
unverifiedSceneNumbering={unverifiedSceneNumbering}
seriesType={seriesType}
sceneSeasonNumber={sceneSeasonNumber}
sceneEpisodeNumber={sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}
alternateTitles={alternateTitles}
/>
</TableRowCell>
);
}
if (name === 'title') {
return (
<TableRowCell
key={name}
className={styles.title}
>
<EpisodeTitleLink
episodeId={id}
artistId={artistId}
episodeTitle={title}
showOpenSeriesButton={false}
/>
</TableRowCell>
);
}
if (name === 'path') {
return (
<TableRowCell key={name}>
{
episodeFilePath
}
</TableRowCell>
);
}
if (name === 'relativePath') {
return (
<TableRowCell key={name}>
{
episodeFileRelativePath
}
</TableRowCell>
);
}
if (name === 'airDateUtc') {
return (
<RelativeDateCellConnector
key={name}
date={airDateUtc}
/>
);
}
if (name === 'language') {
return (
<TableRowCell
key={name}
className={styles.language}
>
<EpisodeFileLanguageConnector
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'audioInfo') {
return (
<TableRowCell
key={name}
className={styles.audio}
>
<MediaInfoConnector
type={mediaInfoTypes.AUDIO}
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'videoCodec') {
return (
<TableRowCell
key={name}
className={styles.video}
>
<MediaInfoConnector
type={mediaInfoTypes.VIDEO}
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'status') {
return (
<TableRowCell
key={name}
className={styles.status}
>
<EpisodeStatusConnector
episodeId={id}
episodeFileId={episodeFileId}
/>
</TableRowCell>
);
}
if (name === 'actions') {
return (
<EpisodeSearchCellConnector
key={name}
episodeId={id}
artistId={artistId}
episodeTitle={title}
/>
);
}
return null;
})
}
</TableRow>
);
}
}
EpisodeRow.propTypes = {
id: PropTypes.number.isRequired,
artistId: PropTypes.number.isRequired,
episodeFileId: PropTypes.number,
monitored: PropTypes.bool.isRequired,
seasonNumber: PropTypes.number.isRequired,
episodeNumber: PropTypes.number.isRequired,
absoluteEpisodeNumber: PropTypes.number,
sceneSeasonNumber: PropTypes.number,
sceneEpisodeNumber: PropTypes.number,
sceneAbsoluteEpisodeNumber: PropTypes.number,
airDateUtc: PropTypes.string,
title: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
unverifiedSceneNumbering: PropTypes.bool,
seriesMonitored: PropTypes.bool.isRequired,
seriesType: PropTypes.string.isRequired,
episodeFilePath: PropTypes.string,
episodeFileRelativePath: PropTypes.string,
mediaInfo: PropTypes.object,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
onMonitorEpisodePress: PropTypes.func.isRequired
};
export default EpisodeRow;

@ -1,70 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { kinds, sizes } from 'Helpers/Props';
import Label from 'Components/Label';
import Link from 'Components/Link/Link';
import styles from './SeriesDetailsLinks.css';
function SeriesDetailsLinks(props) {
const {
foreignArtistId,
tvMazeId,
imdbId
} = props;
return (
<div className={styles.links}>
<Link
className={styles.link}
to={`https://musicbrainz.org/artist/${foreignArtistId}`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
Musicbrainz
</Label>
</Link>
{
!!tvMazeId &&
<Link
className={styles.link}
to={`http://www.tvmaze.com/shows/${tvMazeId}/_`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
TV Maze
</Label>
</Link>
}
{
!!imdbId &&
<Link
className={styles.link}
to={`http://imdb.com/title/${imdbId}/`}
>
<Label
className={styles.linkLabel}
kind={kinds.INFO}
size={sizes.LARGE}
>
IMDB
</Label>
</Link>
}
</div>
);
}
SeriesDetailsLinks.propTypes = {
foreignArtistId: PropTypes.string.isRequired,
tvMazeId: PropTypes.number,
imdbId: PropTypes.string
};
export default SeriesDetailsLinks;

@ -27,7 +27,7 @@ function ArtistStatusCell(props) {
<Icon
className={styles.statusIcon}
name={status === 'ended' ? icons.SERIES_ENDED : icons.SERIES_CONTINUING}
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
title={status === 'ended' ? 'Ended' : 'Continuing'}
/>

@ -7,9 +7,9 @@ export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
export const DELETE_LOG_FILES = 'DeleteLogFiles';
export const DELETE_UPDATE_LOG_FILES = 'DeleteUpdateLogFiles';
export const DOWNLOADED_EPSIODES_SCAN = 'DownloadedEpisodesScan';
export const EPISODE_SEARCH = 'EpisodeSearch';
export const EPISODE_SEARCH = 'AlbumSearch';
export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_EPISODE_SEARCH = 'MissingEpisodeSearch';
export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch';
export const REFRESH_ARTIST = 'RefreshArtist';
export const RENAME_FILES = 'RenameFiles';
export const RENAME_ARTIST = 'RenameArtist';

@ -19,7 +19,7 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.SERIES_CONTINUING,
iconName: icons.ARTIST_CONTINUING,
title: 'Artist',
to: '/',
alias: '/series',

@ -11,7 +11,6 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
import EpisodeSummaryConnector from './Summary/EpisodeSummaryConnector';
import EpisodeHistoryConnector from './History/EpisodeHistoryConnector';
import EpisodeSearchConnector from './Search/EpisodeSearchConnector';
import SeasonEpisodeNumber from './SeasonEpisodeNumber';
import styles from './EpisodeDetailsModalContent.css';
const tabs = [
@ -47,26 +46,22 @@ class EpisodeDetailsModalContent extends Component {
const {
episodeId,
episodeEntity,
episodeFileId,
artistId,
seriesTitle,
titleSlug,
seriesMonitored,
seriesType,
seasonNumber,
episodeNumber,
absoluteEpisodeNumber,
nameSlug,
albumLabel,
artistMonitored,
episodeTitle,
airDate,
releaseDate,
monitored,
isSaving,
showOpenSeriesButton,
startInteractiveSearch,
onMonitorEpisodePress,
onMonitorAlbumPress,
onModalClose
} = this.props;
const seriesLink = `/artist/${titleSlug}`;
const seriesLink = `/artist/${nameSlug}`;
return (
<ModalContent
@ -78,9 +73,9 @@ class EpisodeDetailsModalContent extends Component {
id={episodeId}
monitored={monitored}
size={18}
isDisabled={!seriesMonitored}
isDisabled={!artistMonitored}
isSaving={isSaving}
onPress={onMonitorEpisodePress}
onPress={onMonitorAlbumPress}
/>
<span className={styles.seriesTitle}>
@ -89,16 +84,6 @@ class EpisodeDetailsModalContent extends Component {
<span className={styles.separator}>-</span>
<SeasonEpisodeNumber
seasonNumber={seasonNumber}
episodeNumber={episodeNumber}
absoluteEpisodeNumber={absoluteEpisodeNumber}
airDate={airDate}
seriesType={seriesType}
/>
<span className={styles.separator}>-</span>
{episodeTitle}
</ModalHeader>
@ -137,7 +122,8 @@ class EpisodeDetailsModalContent extends Component {
<EpisodeSummaryConnector
episodeId={episodeId}
episodeEntity={episodeEntity}
episodeFileId={episodeFileId}
releaseDate={releaseDate}
albumLabel={albumLabel}
artistId={artistId}
/>
</TabPanel>
@ -150,7 +136,7 @@ class EpisodeDetailsModalContent extends Component {
<TabPanel className={styles.tabPanel}>
<EpisodeSearchConnector
episodeId={episodeId}
albumId={episodeId}
startInteractiveSearch={startInteractiveSearch}
onModalClose={onModalClose}
/>
@ -166,7 +152,7 @@ class EpisodeDetailsModalContent extends Component {
to={seriesLink}
onPress={onModalClose}
>
Open Series
Open Artist
</Button>
}
@ -184,28 +170,25 @@ class EpisodeDetailsModalContent extends Component {
EpisodeDetailsModalContent.propTypes = {
episodeId: PropTypes.number.isRequired,
episodeEntity: PropTypes.string.isRequired,
episodeFileId: PropTypes.number,
artistId: PropTypes.number.isRequired,
seriesTitle: PropTypes.string.isRequired,
titleSlug: PropTypes.string.isRequired,
seriesMonitored: PropTypes.bool.isRequired,
seriesType: PropTypes.string.isRequired,
seasonNumber: PropTypes.number.isRequired,
episodeNumber: PropTypes.number.isRequired,
absoluteEpisodeNumber: PropTypes.number,
airDate: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired,
artistMonitored: PropTypes.bool.isRequired,
releaseDate: PropTypes.string.isRequired,
albumLabel: PropTypes.string.isRequired,
episodeTitle: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
isSaving: PropTypes.bool,
showOpenSeriesButton: PropTypes.bool,
selectedTab: PropTypes.string.isRequired,
startInteractiveSearch: PropTypes.bool.isRequired,
onMonitorEpisodePress: PropTypes.func.isRequired,
onMonitorAlbumPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
EpisodeDetailsModalContent.defaultProps = {
selectedTab: 'details',
albumLabel: 'Unknown',
episodeEntity: episodeEntities.EPISODES,
startInteractiveSearch: false
};

@ -15,16 +15,16 @@ function createMapStateToProps() {
createArtistSelector(),
(episode, series) => {
const {
title: seriesTitle,
titleSlug,
monitored: seriesMonitored,
artistName: seriesTitle,
nameSlug,
monitored: artistMonitored,
seriesType
} = series;
return {
seriesTitle,
titleSlug,
seriesMonitored,
nameSlug,
artistMonitored,
seriesType,
...episode
};
@ -52,7 +52,7 @@ class EpisodeDetailsModalContentConnector extends Component {
//
// Listeners
onMonitorEpisodePress = (monitored) => {
onMonitorAlbumPress = (monitored) => {
const {
episodeId,
episodeEntity
@ -72,7 +72,7 @@ class EpisodeDetailsModalContentConnector extends Component {
return (
<EpisodeDetailsModalContent
{...this.props}
onMonitorEpisodePress={this.onMonitorEpisodePress}
onMonitorAlbumPress={this.onMonitorAlbumPress}
/>
);
}

@ -21,11 +21,11 @@ function createMapStateToProps() {
return false;
}
return command.body.episodeIds.indexOf(episodeId) > -1;
return command.body.albumIds.indexOf(episodeId) > -1;
});
return {
seriesMonitored: series.monitored,
artistMonitored: series.monitored,
seriesType: series.seriesType,
isSearching
};
@ -38,7 +38,7 @@ function createMapDispatchToProps(dispatch, props) {
onSearchPress(name, path) {
dispatch(executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: [props.episodeId]
albumIds: [props.episodeId]
}));
}
};

@ -61,13 +61,13 @@ class EpisodeHistory extends Component {
if (!isFetching && !!error) {
return (
<div>Unable to load episode history.</div>
<div>Unable to load album history.</div>
);
}
if (isPopulated && !hasItems && !error) {
return (
<div>No episode history.</div>
<div>No album history.</div>
);
}

@ -47,7 +47,7 @@ class EpisodeSearchConnector extends Component {
onQuickSearchPress = () => {
this.props.executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: [this.props.episodeId]
albumIds: [this.props.albumId]
});
this.props.onModalClose();
@ -80,7 +80,7 @@ class EpisodeSearchConnector extends Component {
}
EpisodeSearchConnector.propTypes = {
episodeId: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
isPopulated: PropTypes.bool.isRequired,
startInteractiveSearch: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired,

@ -34,7 +34,7 @@ class InteractiveEpisodeSearchConnector extends Component {
componentDidMount() {
const {
episodeId,
albumId,
isPopulated
} = this.props;
@ -43,7 +43,7 @@ class InteractiveEpisodeSearchConnector extends Component {
if (!isPopulated) {
this.props.fetchReleases({
episodeId
albumId
});
}
}
@ -74,7 +74,7 @@ class InteractiveEpisodeSearchConnector extends Component {
}
InteractiveEpisodeSearchConnector.propTypes = {
episodeId: PropTypes.number.isRequired,
albumId: PropTypes.number.isRequired,
isPopulated: PropTypes.bool.isRequired,
fetchReleases: PropTypes.func.isRequired,
setReleasesSort: PropTypes.func.isRequired,

@ -10,8 +10,8 @@ import Label from 'Components/Label';
function EpisodeAiring(props) {
const {
airDateUtc,
network,
releaseDate,
albumLabel,
shortDateFormat,
showRelativeDates,
timeFormat
@ -22,11 +22,11 @@ function EpisodeAiring(props) {
kind={kinds.INFO}
size={sizes.MEDIUM}
>
{network}
{albumLabel}
</Label>
);
if (!airDateUtc) {
if (!releaseDate) {
return (
<span>
TBA on {networkLabel}
@ -34,50 +34,48 @@ function EpisodeAiring(props) {
);
}
const time = formatTime(airDateUtc, timeFormat);
if (!showRelativeDates) {
return (
<span>
{moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel}
{moment(releaseDate).format(shortDateFormat)} on {networkLabel}
</span>
);
}
if (isToday(airDateUtc)) {
if (isToday(releaseDate)) {
return (
<span>
{time} on {networkLabel}
Today on {networkLabel}
</span>
);
}
if (isTomorrow(airDateUtc)) {
if (isTomorrow(releaseDate)) {
return (
<span>
Tomorrow at {time} on {networkLabel}
Tomorrow on {networkLabel}
</span>
);
}
if (isInNextWeek(airDateUtc)) {
if (isInNextWeek(releaseDate)) {
return (
<span>
{moment(airDateUtc).format('dddd')} at {time} on {networkLabel}
{moment(releaseDate).format('dddd')} on {networkLabel}
</span>
);
}
return (
<span>
{moment(airDateUtc).format(shortDateFormat)} at {time} on {networkLabel}
{moment(releaseDate).format(shortDateFormat)} on {networkLabel}
</span>
);
}
EpisodeAiring.propTypes = {
airDateUtc: PropTypes.string.isRequired,
network: PropTypes.string.isRequired,
releaseDate: PropTypes.string.isRequired,
albumLabel: PropTypes.string.isRequired,
shortDateFormat: PropTypes.string.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
timeFormat: PropTypes.string.isRequired

@ -45,9 +45,9 @@ class EpisodeSummary extends Component {
render() {
const {
qualityProfileId,
network,
overview,
airDateUtc,
releaseDate,
albumLabel,
path,
size,
quality,
@ -59,11 +59,11 @@ class EpisodeSummary extends Component {
return (
<div>
<div>
<span className={styles.infoTitle}>Airs</span>
<span className={styles.infoTitle}>Releases</span>
<EpisodeAiringConnector
airDateUtc={airDateUtc}
network={network}
releaseDate={releaseDate}
albumLabel={albumLabel}
/>
</div>
@ -84,7 +84,7 @@ class EpisodeSummary extends Component {
{
hasOverview ?
overview :
'No episode overview.'
'No album overview.'
}
</div>
@ -151,11 +151,10 @@ class EpisodeSummary extends Component {
}
EpisodeSummary.propTypes = {
episodeFileId: PropTypes.number.isRequired,
qualityProfileId: PropTypes.number.isRequired,
network: PropTypes.string.isRequired,
overview: PropTypes.string,
airDateUtc: PropTypes.string.isRequired,
albumLabel: PropTypes.string,
releaseDate: PropTypes.string.isRequired,
path: PropTypes.string,
size: PropTypes.number,
quality: PropTypes.object,

@ -67,8 +67,8 @@ export const RSS = 'fa fa-rss';
export const SAVE = 'fa fa-floppy-o';
export const SCHEDULED = 'fa fa-clock-o';
export const SEARCH = 'fa fa-search';
export const SERIES_CONTINUING = 'fa fa-play';
export const SERIES_ENDED = 'fa fa-stop';
export const ARTIST_CONTINUING = 'fa fa-play';
export const ARTIST_ENDED = 'fa fa-stop';
export const SETTINGS = 'fa fa-cogs';
export const SHUTDOWN = 'fa fa-power-off';
export const SORT = 'fa fa-sort';

@ -9,7 +9,7 @@ import { updateItem } from './baseActions';
const section = 'episodes';
const episodeActionHandlers = {
[types.FETCH_EPISODES]: createFetchHandler(section, '/track'),
[types.FETCH_EPISODES]: createFetchHandler(section, '/album'),
[types.TOGGLE_EPISODE_MONITORED]: function(payload) {
return function(dispatch, getState) {
@ -28,7 +28,7 @@ const episodeActionHandlers = {
}));
const promise = $.ajax({
url: `/episode/${id}`,
url: `/album/${id}`,
method: 'PUT',
data: JSON.stringify({ monitored }),
dataType: 'json'
@ -56,7 +56,7 @@ const episodeActionHandlers = {
[types.TOGGLE_EPISODES_MONITORED]: function(payload) {
return function(dispatch, getState) {
const {
episodeIds,
albumIds,
episodeEntity = episodeEntities.EPISODES,
monitored
} = payload;
@ -64,7 +64,7 @@ const episodeActionHandlers = {
const episodeSection = _.last(episodeEntity.split('.'));
dispatch(batchActions(
episodeIds.map((episodeId) => {
albumIds.map((episodeId) => {
return updateItem({
id: episodeId,
section: episodeSection,
@ -74,15 +74,15 @@ const episodeActionHandlers = {
));
const promise = $.ajax({
url: '/episode/monitor',
url: '/album/monitor',
method: 'PUT',
data: JSON.stringify({ episodeIds, monitored }),
data: JSON.stringify({ albumIds, monitored }),
dataType: 'json'
});
promise.done((data) => {
dispatch(batchActions(
episodeIds.map((episodeId) => {
albumIds.map((episodeId) => {
return updateItem({
id: episodeId,
section: episodeSection,
@ -95,7 +95,7 @@ const episodeActionHandlers = {
promise.fail((xhr) => {
dispatch(batchActions(
episodeIds.map((episodeId) => {
albumIds.map((episodeId) => {
return updateItem({
id: episodeId,
section: episodeSection,

@ -11,7 +11,7 @@ export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
sortKey: 'episodeNumber',
sortKey: 'releaseDate',
sortDirection: sortDirections.DESCENDING,
items: [],
@ -22,11 +22,6 @@ export const defaultState = {
isVisible: true,
isModifiable: false
},
{
name: 'episodeNumber',
label: '#',
isVisible: true
},
{
name: 'title',
label: 'Title',
@ -38,28 +33,18 @@ export const defaultState = {
isVisible: false
},
{
name: 'relativePath',
label: 'Relative Path',
isVisible: false
},
{
name: 'airDateUtc',
label: 'Air Date',
name: 'releaseDate',
label: 'Release Date',
isVisible: true
},
{
name: 'language',
label: 'Language',
isVisible: false
},
{
name: 'audioInfo',
label: 'Audio Info',
name: 'trackCount',
label: 'Track Count',
isVisible: false
},
{
name: 'videoCodec',
label: 'Video Codec',
name: 'duration',
label: 'Duration',
isVisible: false
},
{

@ -110,7 +110,7 @@ class CutoffUnmetConnector extends Component {
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: selected
albumIds: selected
});
}

@ -18,7 +18,7 @@ function createMapStateToProps() {
createCommandsSelector(),
(missing, commands) => {
const isSearchingForAlbums = _.some(commands, { name: commandNames.EPISODE_SEARCH });
const isSearchingForMissingAlbums = _.some(commands, { name: commandNames.MISSING_EPISODE_SEARCH });
const isSearchingForMissingAlbums = _.some(commands, { name: commandNames.MISSING_ALBUM_SEARCH });
return {
isSearchingForAlbums,
@ -100,7 +100,7 @@ class MissingConnector extends Component {
onSearchSelectedPress = (selected) => {
this.props.executeCommand({
name: commandNames.EPISODE_SEARCH,
episodeIds: selected
albumIds: selected
});
}
@ -118,7 +118,7 @@ class MissingConnector extends Component {
onSearchAllMissingPress = () => {
this.props.executeCommand({
name: commandNames.MISSING_EPISODE_SEARCH
name: commandNames.MISSING_ALBUM_SEARCH
});
}

Loading…
Cancel
Save