You can also search using MusicBrainz ID of a show. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234
You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234
Why can't I find my artist?
@@ -157,7 +157,7 @@ class AddNewArtist extends Component {
!term &&
It's easy to add a new artist, just start typing the name the artist you want to add.
-
You can also search using MusicBrainz ID of a show. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234
+
You can also search using MusicBrainz ID of an artist. eg. lidarr:cc197bad-dc9c-440d-a5b5-d52ba2e14234
}
diff --git a/frontend/src/Album/AlbumCover.js b/frontend/src/Album/AlbumCover.js
new file mode 100644
index 000000000..a6f63c429
--- /dev/null
+++ b/frontend/src/Album/AlbumCover.js
@@ -0,0 +1,168 @@
+import _ from 'lodash';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import LazyLoad from 'react-lazyload';
+
+const coverPlaceholder = 'data:image/png;base64,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';
+
+function findCover(images) {
+ return _.find(images, { coverType: 'cover' });
+}
+
+function getCoverUrl(cover, size) {
+ if (cover) {
+ if (cover.url.contains('lastWrite=') || (/^https?:/).test(cover.url)) {
+ // Remove protocol
+ let url = cover.url.replace(/^https?:/, '');
+ url = url.replace('cover.jpg', `cover-${size}.jpg`);
+
+ return url;
+ }
+ }
+}
+
+class AlbumCover extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ const pixelRatio = Math.floor(window.devicePixelRatio);
+
+ const {
+ images,
+ size
+ } = props;
+
+ const cover = findCover(images);
+
+ this.state = {
+ pixelRatio,
+ cover,
+ coverUrl: getCoverUrl(cover, pixelRatio * size),
+ isLoaded: false,
+ hasError: false
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ images,
+ size
+ } = this.props;
+
+ const {
+ cover,
+ pixelRatio
+ } = this.state;
+
+ const nextcover = findCover(images);
+
+ if (nextcover && (!cover || nextcover.url !== cover.url)) {
+ this.setState({
+ cover: nextcover,
+ coverUrl: getCoverUrl(nextcover, pixelRatio * size),
+ hasError: false
+ // Don't reset isLoaded, as we want to immediately try to
+ // show the new image, whether an image was shown previously
+ // or the placeholder was shown.
+ });
+ }
+ }
+
+ //
+ // Listeners
+
+ onError = () => {
+ this.setState({ hasError: true });
+ }
+
+ onLoad = () => {
+ this.setState({
+ isLoaded: true,
+ hasError: false
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ className,
+ style,
+ size,
+ lazy,
+ overflow
+ } = this.props;
+
+ const {
+ coverUrl,
+ hasError,
+ isLoaded
+ } = this.state;
+
+ if (hasError || !coverUrl) {
+ return (
+

+ );
+ }
+
+ if (lazy) {
+ return (
+
+ }
+ >
+

+
+ );
+ }
+
+ return (
+

+ );
+ }
+}
+
+AlbumCover.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
+};
+
+AlbumCover.defaultProps = {
+ size: 250,
+ lazy: true,
+ overflow: false
+};
+
+export default AlbumCover;
diff --git a/frontend/src/Album/AlbumDetailsModalContent.css b/frontend/src/Album/AlbumDetailsModalContent.css
index 9d428208d..b80b14cb9 100644
--- a/frontend/src/Album/AlbumDetailsModalContent.css
+++ b/frontend/src/Album/AlbumDetailsModalContent.css
@@ -37,7 +37,7 @@
margin-top: 20px;
}
-.openSeriesButton {
+.openArtistButton {
composes: button from 'Components/Link/Button.css';
margin-right: auto;
diff --git a/frontend/src/Album/AlbumDetailsModalContent.js b/frontend/src/Album/AlbumDetailsModalContent.js
index 42a8357d0..178be8086 100644
--- a/frontend/src/Album/AlbumDetailsModalContent.js
+++ b/frontend/src/Album/AlbumDetailsModalContent.js
@@ -8,13 +8,11 @@ import ModalHeader from 'Components/Modal/ModalHeader';
import ModalBody from 'Components/Modal/ModalBody';
import ModalFooter from 'Components/Modal/ModalFooter';
import MonitorToggleButton from 'Components/MonitorToggleButton';
-import AlbumSummaryConnector from './Summary/AlbumSummaryConnector';
import AlbumHistoryConnector from './History/AlbumHistoryConnector';
import AlbumSearchConnector from './Search/AlbumSearchConnector';
import styles from './AlbumDetailsModalContent.css';
const tabs = [
- 'details',
'history',
'search'
];
@@ -45,14 +43,10 @@ class AlbumDetailsModalContent extends Component {
render() {
const {
albumId,
- albumEntity,
- artistId,
artistName,
- nameSlug,
- albumLabel,
+ foreignArtistId,
artistMonitored,
albumTitle,
- releaseDate,
monitored,
isSaving,
showOpenArtistButton,
@@ -61,7 +55,7 @@ class AlbumDetailsModalContent extends Component {
onModalClose
} = this.props;
- const artistLink = `/artist/${nameSlug}`;
+ const artistLink = `/artist/${foreignArtistId}`;
return (
-
- Details
-
-
-
-
-
-
@@ -172,7 +149,7 @@ AlbumDetailsModalContent.propTypes = {
albumEntity: PropTypes.string.isRequired,
artistId: PropTypes.number.isRequired,
artistName: PropTypes.string.isRequired,
- nameSlug: PropTypes.string.isRequired,
+ foreignArtistId: PropTypes.string.isRequired,
artistMonitored: PropTypes.bool.isRequired,
releaseDate: PropTypes.string.isRequired,
albumLabel: PropTypes.arrayOf(PropTypes.string).isRequired,
@@ -187,7 +164,7 @@ AlbumDetailsModalContent.propTypes = {
};
AlbumDetailsModalContent.defaultProps = {
- selectedTab: 'details',
+ selectedTab: 'history',
albumLabel: ['Unknown'],
albumEntity: albumEntities.ALBUMS,
startInteractiveSearch: false
diff --git a/frontend/src/Album/AlbumDetailsModalContentConnector.js b/frontend/src/Album/AlbumDetailsModalContentConnector.js
index 23bfaeeea..5f2c52690 100644
--- a/frontend/src/Album/AlbumDetailsModalContentConnector.js
+++ b/frontend/src/Album/AlbumDetailsModalContentConnector.js
@@ -17,13 +17,13 @@ function createMapStateToProps() {
(album, artist) => {
const {
artistName,
- nameSlug,
+ foreignArtistId,
monitored: artistMonitored
} = artist;
return {
artistName,
- nameSlug,
+ foreignArtistId,
artistMonitored,
...album
};
diff --git a/frontend/src/Album/AlbumSearchCell.js b/frontend/src/Album/AlbumSearchCell.js
index e2ad58550..c1924aa16 100644
--- a/frontend/src/Album/AlbumSearchCell.js
+++ b/frontend/src/Album/AlbumSearchCell.js
@@ -5,6 +5,7 @@ import IconButton from 'Components/Link/IconButton';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import AlbumDetailsModal from './AlbumDetailsModal';
+import EditAlbumModalConnector from './Edit/EditAlbumModalConnector';
import styles from './AlbumSearchCell.css';
class AlbumSearchCell extends Component {
@@ -16,7 +17,8 @@ class AlbumSearchCell extends Component {
super(props, context);
this.state = {
- isDetailsModalOpen: false
+ isDetailsModalOpen: false,
+ isEditAlbumModalOpen: false
};
}
@@ -31,6 +33,14 @@ class AlbumSearchCell extends Component {
this.setState({ isDetailsModalOpen: false });
}
+ onEditAlbumPress = () => {
+ this.setState({ isEditAlbumModalOpen: true });
+ }
+
+ onEditAlbumModalClose = () => {
+ this.setState({ isEditAlbumModalOpen: false });
+ }
+
//
// Render
@@ -57,6 +67,12 @@ class AlbumSearchCell extends Component {
onPress={this.onManualSearchPress}
/>
+
+
+
+
);
}
diff --git a/frontend/src/Album/AlbumTitleDetailLink.js b/frontend/src/Album/AlbumTitleDetailLink.js
new file mode 100644
index 000000000..f357e5a3f
--- /dev/null
+++ b/frontend/src/Album/AlbumTitleDetailLink.js
@@ -0,0 +1,20 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Link from 'Components/Link/Link';
+
+function AlbumTitleDetailLink({ foreignAlbumId, title }) {
+ const link = `/album/${foreignAlbumId}`;
+
+ return (
+
+ {title}
+
+ );
+}
+
+AlbumTitleDetailLink.propTypes = {
+ foreignAlbumId: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired
+};
+
+export default AlbumTitleDetailLink;
diff --git a/frontend/src/Album/Details/AlbumDetails.css b/frontend/src/Album/Details/AlbumDetails.css
new file mode 100644
index 000000000..8b5f5c770
--- /dev/null
+++ b/frontend/src/Album/Details/AlbumDetails.css
@@ -0,0 +1,137 @@
+.innerContentBody {
+ padding: 0;
+}
+
+.header {
+ position: relative;
+ width: 100%;
+ height: 310px;
+}
+
+.backdrop {
+ position: absolute;
+ z-index: -1;
+ width: 100%;
+ height: 100%;
+ background-size: cover;
+}
+
+.backdropOverlay {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ background: $black;
+ opacity: 0.7;
+}
+
+.headerContent {
+ display: flex;
+ padding: 30px;
+ width: 100%;
+ height: 100%;
+ color: $white;
+}
+
+.logo {
+ flex-shrink: 0;
+ margin-right: 35px;
+ width: 250px;
+ height: 97px;
+}
+
+.cover {
+ flex-shrink: 0;
+ margin-right: 35px;
+ width: 250px;
+ height: 250px;
+}
+
+.info {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ overflow: hidden;
+}
+
+.titleRow {
+ display: flex;
+ justify-content: space-between;
+ flex: 0 0 auto;
+}
+
+.titleContainer {
+ display: flex;
+ justify-content: space-between;
+}
+
+.title {
+ margin-bottom: 5px;
+ font-weight: 300;
+ font-size: 50px;
+ line-height: 50px;
+}
+
+.alternateTitlesIconContainer {
+ margin-left: 20px;
+ line-height: 50px;
+}
+
+.artistNavigationButtons {
+ white-space: no-wrap;
+}
+
+.artistNavigationButton {
+ composes: button from 'Components/Link/IconButton.css';
+
+ margin-left: 5px;
+ color: #e1e2e3;
+ white-space: nowrap;
+}
+
+.details {
+ font-weight: 300;
+ font-size: 20px;
+}
+
+.runtime {
+ margin-right: 15px;
+}
+
+.detailsLabel {
+ composes: label from 'Components/Label.css';
+
+ margin: 5px 10px 5px 0;
+}
+
+.sizeOnDisk,
+.qualityProfileName,
+.tags {
+ margin-left: 8px;
+ font-weight: 300;
+ font-size: 17px;
+}
+
+.overview {
+ flex: 1 0 auto;
+ min-height: 0;
+}
+
+.contentContainer {
+ padding: 20px;
+}
+
+@media only screen and (max-width: $breakpointSmall) {
+ .contentContainer {
+ padding: 20px 0;
+ }
+
+ .headerContent {
+ padding: 15px;
+ }
+}
+
+@media only screen and (max-width: $breakpointLarge) {
+ .cover {
+ display: none;
+ }
+}
diff --git a/frontend/src/Album/Details/AlbumDetails.js b/frontend/src/Album/Details/AlbumDetails.js
new file mode 100644
index 000000000..b0d8f9d10
--- /dev/null
+++ b/frontend/src/Album/Details/AlbumDetails.js
@@ -0,0 +1,422 @@
+import _ from 'lodash';
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import formatBytes from 'Utilities/Number/formatBytes';
+import selectAll from 'Utilities/Table/selectAll';
+import toggleSelected from 'Utilities/Table/toggleSelected';
+import { align, icons, sizes } from 'Helpers/Props';
+import HeartRating from 'Components/HeartRating';
+import Icon from 'Components/Icon';
+import IconButton from 'Components/Link/IconButton';
+import Label from 'Components/Label';
+import AlbumCover from 'Album/AlbumCover';
+import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import PageContent from 'Components/Page/PageContent';
+import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
+import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
+import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
+import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
+import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
+import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
+import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
+import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
+
+import styles from './AlbumDetails.css';
+
+function getFanartUrl(images) {
+ const fanartImage = _.find(images, { coverType: 'fanart' });
+ if (fanartImage) {
+ // Remove protocol
+ return fanartImage.url.replace(/^https?:/, '');
+ }
+}
+
+function getExpandedState(newState) {
+ return {
+ allExpanded: newState.allSelected,
+ allCollapsed: newState.allUnselected,
+ expandedState: newState.selectedState
+ };
+}
+
+class AlbumDetails extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isArtistHistoryModalOpen: false,
+ isManageTracksOpen: false,
+ isEditAlbumModalOpen: false,
+ allExpanded: false,
+ allCollapsed: false,
+ expandedState: {}
+ };
+ }
+
+ //
+ // Listeners
+
+ onEditAlbumPress = () => {
+ this.setState({ isEditAlbumModalOpen: true });
+ }
+
+ onEditAlbumModalClose = () => {
+ this.setState({ isEditAlbumModalOpen: false });
+ }
+
+ onManageTracksPress = () => {
+ this.setState({ isManageTracksOpen: true });
+ }
+
+ onManageTracksModalClose = () => {
+ this.setState({ isManageTracksOpen: false });
+ }
+
+ onArtistHistoryPress = () => {
+ this.setState({ isArtistHistoryModalOpen: true });
+ }
+
+ onArtistHistoryModalClose = () => {
+ this.setState({ isArtistHistoryModalOpen: false });
+ }
+
+ onExpandAllPress = () => {
+ const {
+ allExpanded,
+ expandedState
+ } = this.state;
+
+ this.setState(getExpandedState(selectAll(expandedState, !allExpanded)));
+ }
+
+ onExpandPress = (albumId, isExpanded) => {
+ this.setState((state) => {
+ const convertedState = {
+ allSelected: state.allExpanded,
+ allUnselected: state.allCollapsed,
+ selectedState: state.expandedState
+ };
+
+ const newState = toggleSelected(convertedState, [], albumId, isExpanded, false);
+
+ return getExpandedState(newState);
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ id,
+ title,
+ albumType,
+ statistics,
+ monitored,
+ releaseDate,
+ images,
+ media,
+ isFetching,
+ isPopulated,
+ albumsError,
+ trackFilesError,
+ shortDateFormat,
+ artist,
+ previousAlbum,
+ nextAlbum,
+ isSearching,
+ onSearchPress
+ } = this.props;
+
+ const {
+ isArtistHistoryModalOpen,
+ isEditAlbumModalOpen,
+ isManageTracksOpen,
+ allExpanded,
+ allCollapsed,
+ expandedState
+ } = this.state;
+
+ let expandIcon = icons.EXPAND_INDETERMINATE;
+
+ if (allExpanded) {
+ expandIcon = icons.COLLAPSE;
+ } else if (allCollapsed) {
+ expandIcon = icons.EXPAND;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+ {/*
*/}
+
+
+
+
+
+
+
+
+
+ {
+ !!albumType &&
+
+ }
+
+
+
+
+
+
+
+ {
+ !isPopulated && !albumsError && !trackFilesError &&
+
+ }
+
+ {
+ !isFetching && albumsError &&
+
Loading albums failed
+ }
+
+ {
+ !isFetching && trackFilesError &&
+
Loading track files failed
+ }
+
+ {
+ isPopulated && !!media.length &&
+
+
+ {
+ media.slice(0).map((medium) => {
+ return (
+
+ );
+ })
+ }
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+AlbumDetails.propTypes = {
+ id: PropTypes.number.isRequired,
+ foreignAlbumId: PropTypes.string.isRequired,
+ title: PropTypes.string.isRequired,
+ albumType: PropTypes.string.isRequired,
+ statistics: PropTypes.object.isRequired,
+ releaseDate: PropTypes.string.isRequired,
+ images: PropTypes.arrayOf(PropTypes.object).isRequired,
+ media: PropTypes.arrayOf(PropTypes.object).isRequired,
+ monitored: PropTypes.bool.isRequired,
+ shortDateFormat: PropTypes.string.isRequired,
+ isSearching: PropTypes.bool,
+ isFetching: PropTypes.bool,
+ isPopulated: PropTypes.bool,
+ albumsError: PropTypes.object,
+ tracksError: PropTypes.object,
+ trackFilesError: PropTypes.object,
+ artist: PropTypes.object,
+ previousAlbum: PropTypes.object,
+ nextAlbum: PropTypes.object,
+ onRefreshPress: PropTypes.func,
+ onSearchPress: PropTypes.func.isRequired
+};
+
+AlbumDetails.defaultProps = {
+ isSaving: false
+};
+
+export default AlbumDetails;
diff --git a/frontend/src/Album/Details/AlbumDetailsConnector.js b/frontend/src/Album/Details/AlbumDetailsConnector.js
new file mode 100644
index 000000000..e10450745
--- /dev/null
+++ b/frontend/src/Album/Details/AlbumDetailsConnector.js
@@ -0,0 +1,146 @@
+/* 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 { findCommand } from 'Utilities/Command';
+import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
+import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
+import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
+import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
+import { executeCommand } from 'Store/Actions/commandActions';
+import * as commandNames from 'Commands/commandNames';
+import AlbumDetails from './AlbumDetails';
+import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state, { foreignAlbumId }) => foreignAlbumId,
+ (state) => state.tracks,
+ (state) => state.trackFiles,
+ (state) => state.albums,
+ createAllArtistSelector(),
+ createCommandsSelector(),
+ createUISettingsSelector(),
+ (foreignAlbumId, tracks, trackFiles, albums, artists, commands, uiSettings) => {
+ const sortedAlbums = _.orderBy(albums.items, 'releaseDate');
+ const albumIndex = _.findIndex(sortedAlbums, { foreignAlbumId });
+ const album = sortedAlbums[albumIndex];
+ const artist = _.find(artists, { id: album.artistId });
+
+ if (!album) {
+ return {};
+ }
+
+ const previousAlbum = sortedAlbums[albumIndex - 1] || _.last(sortedAlbums);
+ const nextAlbum = sortedAlbums[albumIndex + 1] || _.first(sortedAlbums);
+ const isSearching = !!findCommand(commands, { name: commandNames.ALBUM_SEARCH });
+
+ const isFetching = tracks.isFetching || trackFiles.isFetching;
+ const isPopulated = tracks.isPopulated && trackFiles.isPopulated;
+ const tracksError = tracks.error;
+ const trackFilesError = trackFiles.error;
+
+ return {
+ ...album,
+ shortDateFormat: uiSettings.shortDateFormat,
+ artist,
+ isSearching,
+ isFetching,
+ isPopulated,
+ tracksError,
+ trackFilesError,
+ previousAlbum,
+ nextAlbum
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ executeCommand,
+ fetchTracks,
+ clearTracks,
+ fetchTrackFiles,
+ clearTrackFiles
+};
+
+class AlbumDetailsConnector extends Component {
+
+ componentDidMount() {
+ registerPagePopulator(this.populate);
+ this.populate();
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ id
+ } = this.props;
+
+ // If the id has changed we need to clear the tracks/track
+ // files and fetch from the server.
+
+ if (prevProps.id !== id) {
+ this.unpopulate();
+ this.populate();
+ }
+ }
+
+ componentWillUnmount() {
+ unregisterPagePopulator(this.populate);
+ this.unpopulate();
+ }
+
+ //
+ // Control
+
+ populate = () => {
+ const albumId = this.props.id;
+
+ this.props.fetchTracks({ albumId });
+ this.props.fetchTrackFiles({ albumId });
+ }
+
+ unpopulate = () => {
+ this.props.clearTracks();
+ this.props.clearTrackFiles();
+ }
+
+ //
+ // Listeners
+
+ onSearchPress = () => {
+ this.props.executeCommand({
+ name: commandNames.ALBUM_SEARCH,
+ albumIds: [this.props.id]
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+AlbumDetailsConnector.propTypes = {
+ id: PropTypes.number,
+ isAlbumFetching: PropTypes.bool,
+ isAlbumPopulated: PropTypes.bool,
+ foreignAlbumId: PropTypes.string.isRequired,
+ fetchTracks: PropTypes.func.isRequired,
+ clearTracks: PropTypes.func.isRequired,
+ fetchTrackFiles: PropTypes.func.isRequired,
+ clearTrackFiles: PropTypes.func.isRequired,
+ executeCommand: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsConnector);
diff --git a/frontend/src/Album/Details/AlbumDetailsMedium.css b/frontend/src/Album/Details/AlbumDetailsMedium.css
new file mode 100644
index 000000000..a89cca5f8
--- /dev/null
+++ b/frontend/src/Album/Details/AlbumDetailsMedium.css
@@ -0,0 +1,114 @@
+.medium {
+ margin-bottom: 20px;
+ border: 1px solid $borderColor;
+ border-radius: 4px;
+ background-color: $white;
+
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+.header {
+ position: relative;
+ display: flex;
+ align-items: center;
+ width: 100%;
+ font-size: 24px;
+}
+
+.mediumNumber {
+ margin-right: 10px;
+ margin-left: 5px;
+}
+
+.mediumFormat {
+ color: #8895aa;
+ font-style: italic;
+ font-size: 18px;
+}
+
+.expandButton {
+ composes: link from 'Components/Link/Link.css';
+
+ flex-grow: 1;
+ margin: 0 20px;
+ text-align: center;
+}
+
+.left {
+ display: flex;
+ align-items: center;
+ flex: 0 1 300px;
+}
+
+.left,
+.actions {
+ padding: 15px 10px;
+}
+
+.actionsMenu {
+ composes: menu from 'Components/Menu/Menu.css';
+
+ flex: 0 0 45px;
+}
+
+.actionsMenuContent {
+ composes: menuContent from 'Components/Menu/MenuContent.css';
+
+ white-space: nowrap;
+ font-size: 14px;
+}
+
+.actionMenuIcon {
+ margin-right: 8px;
+}
+
+.actionButton {
+ composes: button from 'Components/Link/IconButton.css';
+
+ width: 30px;
+}
+
+.tracks {
+ padding-top: 15px;
+ border-top: 1px solid $borderColor;
+}
+
+.collapseButtonContainer {
+ padding: 10px 15px;
+ width: 100%;
+ border-top: 1px solid $borderColor;
+ border-bottom-right-radius: 4px;
+ border-bottom-left-radius: 4px;
+ background-color: #fafafa;
+ text-align: center;
+}
+
+.expandButtonIcon {
+ composes: actionButton;
+
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin-top: -12px;
+ margin-left: -15px;
+}
+
+.noTracks {
+ margin-bottom: 15px;
+ text-align: center;
+}
+
+@media only screen and (max-width: $breakpointSmall) {
+ .medium {
+ border-right: 0;
+ border-left: 0;
+ border-radius: 0;
+ }
+
+ .expandButtonIcon {
+ position: static;
+ margin: 0;
+ }
+}
diff --git a/frontend/src/Album/Details/AlbumDetailsMedium.js b/frontend/src/Album/Details/AlbumDetailsMedium.js
new file mode 100644
index 000000000..0a3d9c242
--- /dev/null
+++ b/frontend/src/Album/Details/AlbumDetailsMedium.js
@@ -0,0 +1,211 @@
+import _ from 'lodash';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import isAfter from 'Utilities/Date/isAfter';
+import isBefore from 'Utilities/Date/isBefore';
+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++;
+ }
+
+ totalTrackCount++;
+ });
+
+ return {
+ trackCount,
+ trackFileCount,
+ totalTrackCount
+ };
+}
+
+function getTrackCountKind(trackFileCount, trackCount) {
+ if (trackFileCount === trackCount && trackCount > 0) {
+ return kinds.SUCCESS;
+ }
+
+ 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,
+ items
+ } = this.props;
+
+ const expand = _.some(items, (item) => {
+ return isAfter(item.airDateUtc) ||
+ isAfter(item.airDateUtc, { days: -30 });
+ });
+
+ onExpandPress(mediumNumber, expand && mediumNumber > 0);
+ }
+
+ //
+ // Listeners
+
+ onExpandPress = () => {
+ const {
+ mediumNumber,
+ isExpanded
+ } = this.props;
+
+ this.props.onExpandPress(mediumNumber, !isExpanded);
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ mediumNumber,
+ mediumFormat,
+ items,
+ columns,
+ onTableOptionChange,
+ isExpanded,
+ isSmallScreen
+ } = this.props;
+
+ const {
+ trackCount,
+ trackFileCount,
+ totalTrackCount
+ } = getMediumStatistics(items);
+
+ return (
+
+
+
+ {
+
+
+ {mediumFormat} {mediumNumber}
+
+
+ }
+
+
+
+
+
+
+ {
+ !isSmallScreen &&
+
+ }
+
+
+
+
+
+ {
+ isExpanded &&
+
+ {
+ items.length ?
+
+
+ {
+ items.map((item) => {
+ return (
+
+ );
+ })
+ }
+
+
:
+
+
+ No tracks in this medium
+
+ }
+
+
+
+
+ }
+
+
+ );
+ }
+}
+
+AlbumDetailsMedium.propTypes = {
+ albumId: PropTypes.number.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;
diff --git a/frontend/src/Album/Details/AlbumDetailsMediumConnector.js b/frontend/src/Album/Details/AlbumDetailsMediumConnector.js
new file mode 100644
index 000000000..32010a224
--- /dev/null
+++ b/frontend/src/Album/Details/AlbumDetailsMediumConnector.js
@@ -0,0 +1,64 @@
+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 (
+
+ );
+ }
+}
+
+AlbumDetailsMediumConnector.propTypes = {
+ albumId: PropTypes.number.isRequired,
+ mediumNumber: PropTypes.number.isRequired,
+ setTracksTableOption: PropTypes.func.isRequired,
+ executeCommand: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsMediumConnector);
diff --git a/frontend/src/Album/Details/AlbumDetailsPageConnector.js b/frontend/src/Album/Details/AlbumDetailsPageConnector.js
new file mode 100644
index 000000000..158906939
--- /dev/null
+++ b/frontend/src/Album/Details/AlbumDetailsPageConnector.js
@@ -0,0 +1,110 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { push } from 'react-router-redux';
+import NotFound from 'Components/NotFound';
+import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import AlbumDetailsConnector from './AlbumDetailsConnector';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state, { match }) => match,
+ (state) => state.albums,
+ (match, albums) => {
+ const foreignAlbumId = match.params.foreignAlbumId;
+ const isAlbumsFetching = albums.isFetching;
+ const isAlbumsPopulated = albums.isPopulated;
+
+ return {
+ foreignAlbumId,
+ isAlbumsFetching,
+ isAlbumsPopulated
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ push,
+ fetchAlbums,
+ clearAlbums
+};
+
+class AlbumDetailsPageConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount() {
+ this.populate();
+ }
+
+ componentWillUnmount() {
+ this.unpopulate();
+ }
+
+ //
+ // Control
+
+ populate = () => {
+ const foreignAlbumId = this.props.foreignAlbumId;
+ this.props.fetchAlbums({ foreignAlbumId });
+ }
+
+ unpopulate = () => {
+ this.props.clearAlbums();
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ foreignAlbumId,
+ isAlbumsFetching,
+ isAlbumsPopulated
+ } = this.props;
+
+ if (!foreignAlbumId) {
+ return (
+
+ );
+ }
+
+ if (isAlbumsFetching) {
+ return (
+
+ );
+ }
+
+ if (!isAlbumsFetching && !isAlbumsPopulated) {
+ return (
+
+ );
+ }
+
+ if (!isAlbumsFetching && isAlbumsPopulated) {
+ return (
+
+ );
+ }
+ }
+}
+
+AlbumDetailsPageConnector.propTypes = {
+ foreignAlbumId: PropTypes.string,
+ match: PropTypes.shape({ params: PropTypes.shape({ foreignAlbumId: PropTypes.string.isRequired }).isRequired }).isRequired,
+ push: PropTypes.func.isRequired,
+ fetchAlbums: PropTypes.func.isRequired,
+ clearAlbums: PropTypes.func.isRequired,
+ isAlbumsFetching: PropTypes.bool.isRequired,
+ isAlbumsPopulated: PropTypes.bool.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(AlbumDetailsPageConnector);
diff --git a/frontend/src/Album/Details/TrackRow.css b/frontend/src/Album/Details/TrackRow.css
new file mode 100644
index 000000000..8497f92e1
--- /dev/null
+++ b/frontend/src/Album/Details/TrackRow.css
@@ -0,0 +1,31 @@
+.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: 200px;
+}
+
+.language,
+.duration,
+.status {
+ composes: cell from 'Components/Table/Cells/TableRowCell.css';
+
+ width: 100px;
+}
diff --git a/frontend/src/Album/Details/TrackRow.js b/frontend/src/Album/Details/TrackRow.js
new file mode 100644
index 000000000..d7cda1fb4
--- /dev/null
+++ b/frontend/src/Album/Details/TrackRow.js
@@ -0,0 +1,194 @@
+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 TrackFileLanguageConnector from 'TrackFile/TrackFileLanguageConnector';
+import MediaInfoConnector from 'TrackFile/MediaInfoConnector';
+import * as mediaInfoTypes from 'TrackFile/mediaInfoTypes';
+
+import styles from './TrackRow.css';
+
+class TrackRow extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isDetailsModalOpen: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onDetailsModalClose = () => {
+ this.setState({ isDetailsModalOpen: false });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ id,
+ mediumNumber,
+ trackFileId,
+ absoluteTrackNumber,
+ title,
+ duration,
+ trackFilePath,
+ trackFileRelativePath,
+ columns
+ } = this.props;
+
+ return (
+
+ {
+ columns.map((column) => {
+ const {
+ name,
+ isVisible
+ } = column;
+
+ if (!isVisible) {
+ return null;
+ }
+
+ if (name === 'medium') {
+ return (
+
+ {mediumNumber}
+
+ );
+ }
+
+ if (name === 'absoluteTrackNumber') {
+ return (
+
+ {absoluteTrackNumber}
+
+ );
+ }
+
+ if (name === 'title') {
+ return (
+
+ {title}
+
+ );
+ }
+
+ if (name === 'path') {
+ return (
+
+ {
+ trackFilePath
+ }
+
+ );
+ }
+
+ if (name === 'relativePath') {
+ return (
+
+ {
+ trackFileRelativePath
+ }
+
+ );
+ }
+
+ if (name === 'duration') {
+ return (
+
+ {
+ formatTimeSpan(duration)
+ }
+
+ );
+ }
+
+ if (name === 'language') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'audioInfo') {
+ return (
+
+
+
+ );
+ }
+
+ if (name === 'status') {
+ return (
+
+
+
+ );
+ }
+
+ return null;
+ })
+ }
+
+ );
+ }
+}
+
+TrackRow.propTypes = {
+ id: PropTypes.number.isRequired,
+ albumId: PropTypes.number.isRequired,
+ trackFileId: PropTypes.number,
+ monitored: PropTypes.bool.isRequired,
+ 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,
+ trackFileRelativePath: PropTypes.string,
+ mediaInfo: PropTypes.object,
+ columns: PropTypes.arrayOf(PropTypes.object).isRequired
+};
+
+export default TrackRow;
diff --git a/frontend/src/Album/Details/TrackRowConnector.js b/frontend/src/Album/Details/TrackRowConnector.js
new file mode 100644
index 000000000..933fa32f5
--- /dev/null
+++ b/frontend/src/Album/Details/TrackRowConnector.js
@@ -0,0 +1,22 @@
+/* eslint max-params: 0 */
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createTrackFileSelector from 'Store/Selectors/createTrackFileSelector';
+import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
+import TrackRow from './TrackRow';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state, { id }) => id,
+ (state, { mediumNumber }) => mediumNumber,
+ createTrackFileSelector(),
+ createCommandsSelector(),
+ (id, mediumNumber, trackFile, commands) => {
+ return {
+ trackFilePath: trackFile ? trackFile.path : null,
+ trackFileRelativePath: trackFile ? trackFile.relativePath : null
+ };
+ }
+ );
+}
+export default connect(createMapStateToProps)(TrackRow);
diff --git a/frontend/src/Album/Edit/EditAlbumModal.js b/frontend/src/Album/Edit/EditAlbumModal.js
new file mode 100644
index 000000000..d47bb284f
--- /dev/null
+++ b/frontend/src/Album/Edit/EditAlbumModal.js
@@ -0,0 +1,25 @@
+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 (
+
+
+
+ );
+}
+
+EditAlbumModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default EditAlbumModal;
diff --git a/frontend/src/Album/Edit/EditAlbumModalConnector.js b/frontend/src/Album/Edit/EditAlbumModalConnector.js
new file mode 100644
index 000000000..7c2383f0f
--- /dev/null
+++ b/frontend/src/Album/Edit/EditAlbumModalConnector.js
@@ -0,0 +1,39 @@
+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 (
+
+ );
+ }
+}
+
+EditAlbumModalConnector.propTypes = {
+ onModalClose: PropTypes.func.isRequired,
+ clearPendingChanges: PropTypes.func.isRequired
+};
+
+export default connect(undefined, mapDispatchToProps)(EditAlbumModalConnector);
diff --git a/frontend/src/Album/Edit/EditAlbumModalContent.js b/frontend/src/Album/Edit/EditAlbumModalContent.js
new file mode 100644
index 000000000..017f3158a
--- /dev/null
+++ b/frontend/src/Album/Edit/EditAlbumModalContent.js
@@ -0,0 +1,119 @@
+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,
+ item,
+ isSaving,
+ onInputChange,
+ onModalClose,
+ ...otherProps
+ } = this.props;
+
+ const {
+ monitored,
+ currentRelease,
+ releases
+ } = item;
+
+ return (
+
+
+ Edit - {artistName} - {title} [{albumType}]
+
+
+
+
+
+
+
+
+
+ Save
+
+
+
+
+ );
+ }
+}
+
+EditAlbumModalContent.propTypes = {
+ albumId: PropTypes.number.isRequired,
+ title: PropTypes.string.isRequired,
+ artistName: PropTypes.string.isRequired,
+ albumType: PropTypes.string.isRequired,
+ item: PropTypes.object.isRequired,
+ isSaving: PropTypes.bool.isRequired,
+ onInputChange: PropTypes.func.isRequired,
+ onSavePress: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default EditAlbumModalContent;
diff --git a/frontend/src/Album/Edit/EditAlbumModalContentConnector.js b/frontend/src/Album/Edit/EditAlbumModalContentConnector.js
new file mode 100644
index 000000000..867e9c0c0
--- /dev/null
+++ b/frontend/src/Album/Edit/EditAlbumModalContentConnector.js
@@ -0,0 +1,97 @@
+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',
+ 'currentRelease',
+ 'releases'
+ ]);
+
+ const settings = selectSettings(albumSettings, pendingChanges, saveError);
+
+ return {
+ title: album.title,
+ artistName: artist.artistName,
+ albumType: album.albumType,
+ 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 (
+
+ );
+ }
+}
+
+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);
diff --git a/frontend/src/AlbumStudio/AlbumStudioRow.js b/frontend/src/AlbumStudio/AlbumStudioRow.js
index d63dc83ee..501d1c74e 100644
--- a/frontend/src/AlbumStudio/AlbumStudioRow.js
+++ b/frontend/src/AlbumStudio/AlbumStudioRow.js
@@ -20,6 +20,7 @@ class AlbumStudioRow extends Component {
artistId,
status,
nameSlug,
+ foreignArtistId,
artistName,
monitored,
albums,
@@ -49,7 +50,7 @@ class AlbumStudioRow extends Component {
@@ -84,6 +85,7 @@ AlbumStudioRow.propTypes = {
artistId: PropTypes.number.isRequired,
status: PropTypes.string.isRequired,
nameSlug: PropTypes.string.isRequired,
+ foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired,
monitored: PropTypes.bool.isRequired,
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
diff --git a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js
index 392e9602c..6172d2c71 100644
--- a/frontend/src/AlbumStudio/AlbumStudioRowConnector.js
+++ b/frontend/src/AlbumStudio/AlbumStudioRowConnector.js
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import { toggleArtistMonitored } from 'Store/Actions/artistActions';
-import { toggleAlbumMonitored } from 'Store/Actions/albumActions';
+import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
import AlbumStudioRow from './AlbumStudioRow';
function createMapStateToProps() {
@@ -32,7 +32,7 @@ function createMapStateToProps() {
const mapDispatchToProps = {
toggleArtistMonitored,
- toggleAlbumMonitored
+ toggleAlbumsMonitored
};
class AlbumStudioRowConnector extends Component {
@@ -53,8 +53,9 @@ class AlbumStudioRowConnector extends Component {
}
onAlbumMonitoredPress = (albumId, monitored) => {
- this.props.toggleAlbumMonitored({
- albumId,
+ const albumIds = [albumId];
+ this.props.toggleAlbumsMonitored({
+ albumIds,
monitored
});
}
@@ -77,7 +78,7 @@ AlbumStudioRowConnector.propTypes = {
artistId: PropTypes.number.isRequired,
monitored: PropTypes.bool.isRequired,
toggleArtistMonitored: PropTypes.func.isRequired,
- toggleAlbumMonitored: PropTypes.func.isRequired
+ toggleAlbumsMonitored: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(AlbumStudioRowConnector);
diff --git a/frontend/src/App/App.js b/frontend/src/App/App.js
index 5a8d6a95c..d7aec6f7a 100644
--- a/frontend/src/App/App.js
+++ b/frontend/src/App/App.js
@@ -14,6 +14,7 @@ import ImportArtist from 'AddArtist/ImportArtist/ImportArtist';
import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector';
import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector';
import ArtistDetailsPageConnector from 'Artist/Details/ArtistDetailsPageConnector';
+import AlbumDetailsPageConnector from 'Album/Details/AlbumDetailsPageConnector';
import CalendarPageConnector from 'Calendar/CalendarPageConnector';
import HistoryConnector from 'Activity/History/HistoryConnector';
import QueueConnector from 'Activity/Queue/QueueConnector';
@@ -92,10 +93,15 @@ function App({ store, history }) {
/>
+
+
{/*
Calendar
*/}
diff --git a/frontend/src/Artist/ArtistNameLink.js b/frontend/src/Artist/ArtistNameLink.js
index aafc97912..fab1cb974 100644
--- a/frontend/src/Artist/ArtistNameLink.js
+++ b/frontend/src/Artist/ArtistNameLink.js
@@ -2,8 +2,8 @@ import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
-function ArtistNameLink({ nameSlug, artistName }) {
- const link = `/artist/${nameSlug}`;
+function ArtistNameLink({ foreignArtistId, artistName }) {
+ const link = `/artist/${foreignArtistId}`;
return (
@@ -13,7 +13,7 @@ function ArtistNameLink({ nameSlug, artistName }) {
}
ArtistNameLink.propTypes = {
- nameSlug: PropTypes.string.isRequired,
+ foreignArtistId: PropTypes.string.isRequired,
artistName: PropTypes.string.isRequired
};
diff --git a/frontend/src/Artist/Details/AlbumRow.js b/frontend/src/Artist/Details/AlbumRow.js
index 973efcbc0..c781b179a 100644
--- a/frontend/src/Artist/Details/AlbumRow.js
+++ b/frontend/src/Artist/Details/AlbumRow.js
@@ -8,8 +8,7 @@ import Label from 'Components/Label';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
import AlbumSearchCellConnector from 'Album/AlbumSearchCellConnector';
-import AlbumTitleLink from 'Album/AlbumTitleLink';
-
+import AlbumTitleDetailLink from 'Album/AlbumTitleDetailLink';
import styles from './AlbumRow.css';
function getTrackCountKind(monitored, trackFileCount, trackCount) {
@@ -33,7 +32,8 @@ class AlbumRow extends Component {
super(props, context);
this.state = {
- isDetailsModalOpen: false
+ isDetailsModalOpen: false,
+ isEditAlbumModalOpen: false
};
}
@@ -48,6 +48,14 @@ class AlbumRow extends Component {
this.setState({ isDetailsModalOpen: false });
}
+ onEditAlbumPress = () => {
+ this.setState({ isEditAlbumModalOpen: true });
+ }
+
+ onEditAlbumModalClose = () => {
+ this.setState({ isEditAlbumModalOpen: false });
+ }
+
onMonitorAlbumPress = (monitored, options) => {
this.props.onMonitorAlbumPress(this.props.id, monitored, options);
}
@@ -64,9 +72,11 @@ class AlbumRow extends Component {
duration,
releaseDate,
mediumCount,
+ secondaryTypes,
title,
isSaving,
artistMonitored,
+ foreignAlbumId,
columns
} = this.props;
@@ -111,11 +121,9 @@ class AlbumRow extends Component {
key={name}
className={styles.title}
>
-
);
@@ -131,6 +139,16 @@ class AlbumRow extends Component {
);
}
+ if (name === 'secondaryTypes') {
+ return (
+
+ {
+ secondaryTypes
+ }
+
+ );
+ }
+
if (name === 'trackCount') {
return (
@@ -189,7 +207,6 @@ class AlbumRow extends Component {
/>
);
}
-
return null;
})
}
@@ -206,6 +223,8 @@ AlbumRow.propTypes = {
mediumCount: PropTypes.number.isRequired,
duration: PropTypes.number.isRequired,
title: PropTypes.string.isRequired,
+ secondaryTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
+ foreignAlbumId: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
unverifiedSceneNumbering: PropTypes.bool,
artistMonitored: PropTypes.bool.isRequired,
diff --git a/frontend/src/Artist/Details/AlbumRowConnector.js b/frontend/src/Artist/Details/AlbumRowConnector.js
index c2260e773..05ae5042c 100644
--- a/frontend/src/Artist/Details/AlbumRowConnector.js
+++ b/frontend/src/Artist/Details/AlbumRowConnector.js
@@ -15,6 +15,7 @@ function createMapStateToProps() {
createCommandsSelector(),
(id, sceneSeasonNumber, artist, trackFile, commands) => {
return {
+ foreignArtistId: artist.foreignArtistId,
artistMonitored: artist.monitored,
trackFilePath: trackFile ? trackFile.path : null,
trackFileRelativePath: trackFile ? trackFile.relativePath : null
diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js
index d9c877e2f..5793cbce2 100644
--- a/frontend/src/Artist/Details/ArtistDetails.js
+++ b/frontend/src/Artist/Details/ArtistDetails.js
@@ -34,34 +34,6 @@ import ArtistDetailsLinks from './ArtistDetailsLinks';
import styles from './ArtistDetails.css';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
-const albumTypes = [
- {
- name: 'album',
- label: 'Album',
- isVisible: true
- },
- {
- name: 'ep',
- label: 'EP',
- isVisible: true
- },
- {
- name: 'single',
- label: 'Single',
- isVisible: true
- },
- {
- name: 'broadcast',
- label: 'Broadcast',
- isVisible: true
- },
- {
- name: 'other',
- label: 'Other',
- isVisible: true
- }
-];
-
const defaultFontSize = parseInt(fonts.defaultFontSize);
const lineHeight = parseFloat(fonts.lineHeight);
@@ -193,6 +165,7 @@ class ArtistDetails extends Component {
trackFileCount,
qualityProfileId,
monitored,
+ albumTypes,
status,
overview,
links,
@@ -359,7 +332,7 @@ class ArtistDetails extends Component {
name={icons.ARROW_LEFT}
size={30}
title={`Go to ${previousArtist.artistName}`}
- to={`/artist/${previousArtist.nameSlug}`}
+ to={`/artist/${previousArtist.foreignArtistId}`}
/>