+ {
+ !!duration &&
+
+ {formatDuration(duration)}
+
+ }
+
-
@@ -487,6 +534,7 @@ AlbumDetails.propTypes = {
foreignAlbumId: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
disambiguation: PropTypes.string,
+ duration: PropTypes.number,
overview: PropTypes.string,
albumType: PropTypes.string.isRequired,
statistics: PropTypes.object.isRequired,
@@ -497,6 +545,7 @@ AlbumDetails.propTypes = {
media: PropTypes.arrayOf(PropTypes.object).isRequired,
monitored: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
+ isSaving: PropTypes.bool.isRequired,
isSearching: PropTypes.bool,
isFetching: PropTypes.bool,
isPopulated: PropTypes.bool,
@@ -506,6 +555,7 @@ AlbumDetails.propTypes = {
artist: PropTypes.object,
previousAlbum: PropTypes.object,
nextAlbum: PropTypes.object,
+ onMonitorTogglePress: PropTypes.func.isRequired,
onRefreshPress: PropTypes.func,
onSearchPress: PropTypes.func.isRequired
};
diff --git a/frontend/src/Album/Details/AlbumDetailsConnector.js b/frontend/src/Album/Details/AlbumDetailsConnector.js
index 9d9585bab..ffb8ffa1f 100644
--- a/frontend/src/Album/Details/AlbumDetailsConnector.js
+++ b/frontend/src/Album/Details/AlbumDetailsConnector.js
@@ -7,6 +7,7 @@ import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
+import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
import { executeCommand } from 'Store/Actions/commandActions';
@@ -64,7 +65,8 @@ const mapDispatchToProps = {
fetchTracks,
clearTracks,
fetchTrackFiles,
- clearTrackFiles
+ clearTrackFiles,
+ toggleAlbumsMonitored
};
function getMonitoredReleases(props) {
@@ -109,6 +111,13 @@ class AlbumDetailsConnector extends Component {
//
// Listeners
+ onMonitorTogglePress = (monitored) => {
+ this.props.toggleAlbumsMonitored({
+ albumIds: [this.props.id],
+ monitored
+ });
+ }
+
onSearchPress = () => {
this.props.executeCommand({
name: commandNames.ALBUM_SEARCH,
@@ -123,6 +132,7 @@ class AlbumDetailsConnector extends Component {
return (
);
@@ -138,6 +148,7 @@ AlbumDetailsConnector.propTypes = {
clearTracks: PropTypes.func.isRequired,
fetchTrackFiles: PropTypes.func.isRequired,
clearTrackFiles: PropTypes.func.isRequired,
+ toggleAlbumsMonitored: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired
};
diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModal.js b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js
new file mode 100644
index 000000000..52e825bab
--- /dev/null
+++ b/frontend/src/Album/Search/AlbumInteractiveSearchModal.js
@@ -0,0 +1,36 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Modal from 'Components/Modal/Modal';
+import AlbumInteractiveSearchModalContent from './AlbumInteractiveSearchModalContent';
+
+function AlbumInteractiveSearchModal(props) {
+ const {
+ isOpen,
+ albumId,
+ albumTitle,
+ onModalClose
+ } = props;
+
+ return (
+
+
+
+ );
+}
+
+AlbumInteractiveSearchModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ albumId: PropTypes.number.isRequired,
+ albumTitle: PropTypes.string.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default AlbumInteractiveSearchModal;
diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModalConnector.js b/frontend/src/Album/Search/AlbumInteractiveSearchModalConnector.js
new file mode 100644
index 000000000..5b23395fb
--- /dev/null
+++ b/frontend/src/Album/Search/AlbumInteractiveSearchModalConnector.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
+import AlbumInteractiveSearchModal from './AlbumInteractiveSearchModal';
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onModalClose() {
+ dispatch(cancelFetchReleases());
+ dispatch(clearReleases());
+ props.onModalClose();
+ }
+ };
+}
+
+export default connect(null, createMapDispatchToProps)(AlbumInteractiveSearchModal);
diff --git a/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js
new file mode 100644
index 000000000..7d94ed946
--- /dev/null
+++ b/frontend/src/Album/Search/AlbumInteractiveSearchModalContent.js
@@ -0,0 +1,47 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Button from 'Components/Link/Button';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
+
+function AlbumInteractiveSearchModalContent(props) {
+ const {
+ albumId,
+ albumTitle,
+ onModalClose
+ } = props;
+
+ return (
+
+
+ Interactive Search {albumId != null && `- ${albumTitle}`}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+AlbumInteractiveSearchModalContent.propTypes = {
+ albumId: PropTypes.number.isRequired,
+ albumTitle: PropTypes.string.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default AlbumInteractiveSearchModalContent;
diff --git a/frontend/src/Album/EpisodeLanguage.js b/frontend/src/Album/TrackLanguage.js
similarity index 80%
rename from frontend/src/Album/EpisodeLanguage.js
rename to frontend/src/Album/TrackLanguage.js
index 52c8b3390..666674814 100644
--- a/frontend/src/Album/EpisodeLanguage.js
+++ b/frontend/src/Album/TrackLanguage.js
@@ -3,7 +3,7 @@ import React from 'react';
import Label from 'Components/Label';
import { kinds } from 'Helpers/Props';
-function EpisodeLanguage(props) {
+function TrackLanguage(props) {
const {
className,
language,
@@ -24,14 +24,14 @@ function EpisodeLanguage(props) {
);
}
-EpisodeLanguage.propTypes = {
+TrackLanguage.propTypes = {
className: PropTypes.string,
language: PropTypes.object,
isCutoffNotMet: PropTypes.bool
};
-EpisodeLanguage.defaultProps = {
+TrackLanguage.defaultProps = {
isCutoffNotMet: true
};
-export default EpisodeLanguage;
+export default TrackLanguage;
diff --git a/frontend/src/App/AppUpdatedModal.js b/frontend/src/App/AppUpdatedModal.js
index 285b87ec8..abc7f8832 100644
--- a/frontend/src/App/AppUpdatedModal.js
+++ b/frontend/src/App/AppUpdatedModal.js
@@ -12,6 +12,7 @@ function AppUpdatedModal(props) {
return (
{
- this.setState({ hasError: true });
- }
-
- onLoad = () => {
- this.setState({
- isLoaded: true,
- hasError: false
- });
- }
-
- //
- // Render
-
- render() {
- const {
- className,
- style,
- size,
- lazy,
- overflow
- } = this.props;
-
- const {
- bannerUrl,
- hasError,
- isLoaded
- } = this.state;
-
- if (hasError || !bannerUrl) {
- return (
-
- );
- }
-
- if (lazy) {
- return (
-
- }
- >
-
-
- );
- }
-
- return (
-
- );
- }
+function ArtistBanner(props) {
+ return (
+
+ );
}
ArtistBanner.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
+ size: PropTypes.number.isRequired
};
ArtistBanner.defaultProps = {
- size: 70,
- lazy: true,
- overflow: false
+ size: 70
};
export default ArtistBanner;
diff --git a/frontend/src/Artist/ArtistImage.js b/frontend/src/Artist/ArtistImage.js
new file mode 100644
index 000000000..576a354a1
--- /dev/null
+++ b/frontend/src/Artist/ArtistImage.js
@@ -0,0 +1,199 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import LazyLoad from 'react-lazyload';
+
+function findImage(images, coverType) {
+ return images.find((image) => image.coverType === coverType);
+}
+
+function getUrl(image, coverType, size) {
+ if (image) {
+ // Remove protocol
+ let url = image.url.replace(/^https?:/, '');
+ url = url.replace(`${coverType}.jpg`, `${coverType}-${size}.jpg`);
+
+ return url;
+ }
+}
+
+class ArtistImage extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ const pixelRatio = Math.floor(window.devicePixelRatio);
+
+ const {
+ images,
+ coverType,
+ size
+ } = props;
+
+ const image = findImage(images, coverType);
+
+ this.state = {
+ pixelRatio,
+ image,
+ url: getUrl(image, coverType, pixelRatio * size),
+ isLoaded: false,
+ hasError: false
+ };
+ }
+
+ componentDidMount() {
+ if (!this.state.url && this.props.onError) {
+ this.props.onError();
+ }
+ }
+
+ componentDidUpdate() {
+ const {
+ images,
+ coverType,
+ placeholder,
+ size,
+ onError
+ } = this.props;
+
+ const {
+ image,
+ pixelRatio
+ } = this.state;
+
+ const nextImage = findImage(images, coverType);
+
+ if (nextImage && (!image || nextImage.url !== image.url)) {
+ this.setState({
+ image: nextImage,
+ url: getUrl(nextImage, coverType, 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.
+ });
+ } else if (!nextImage && image) {
+ this.setState({
+ image: nextImage,
+ url: placeholder,
+ hasError: false
+ });
+
+ if (onError) {
+ onError();
+ }
+ }
+ }
+
+ //
+ // Listeners
+
+ onError = () => {
+ this.setState({
+ hasError: true
+ });
+
+ if (this.props.onError) {
+ this.props.onError();
+ }
+ }
+
+ onLoad = () => {
+ this.setState({
+ isLoaded: true,
+ hasError: false
+ });
+
+ if (this.props.onLoad) {
+ this.props.onLoad();
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ className,
+ style,
+ placeholder,
+ size,
+ lazy,
+ overflow
+ } = this.props;
+
+ const {
+ url,
+ hasError,
+ isLoaded
+ } = this.state;
+
+ if (hasError || !url) {
+ return (
+
+ );
+ }
+
+ if (lazy) {
+ return (
+
+ }
+ >
+
+
+ );
+ }
+
+ return (
+
+ );
+ }
+}
+
+ArtistImage.propTypes = {
+ className: PropTypes.string,
+ style: PropTypes.object,
+ images: PropTypes.arrayOf(PropTypes.object).isRequired,
+ coverType: PropTypes.string.isRequired,
+ placeholder: PropTypes.string.isRequired,
+ size: PropTypes.number.isRequired,
+ lazy: PropTypes.bool.isRequired,
+ overflow: PropTypes.bool.isRequired,
+ onError: PropTypes.func,
+ onLoad: PropTypes.func
+};
+
+ArtistImage.defaultProps = {
+ size: 250,
+ lazy: true,
+ overflow: false
+};
+
+export default ArtistImage;
diff --git a/frontend/src/Artist/ArtistPoster.js b/frontend/src/Artist/ArtistPoster.js
index 21038a9f6..4eebd9ca4 100644
--- a/frontend/src/Artist/ArtistPoster.js
+++ b/frontend/src/Artist/ArtistPoster.js
@@ -1,172 +1,25 @@
-import _ from 'lodash';
import PropTypes from 'prop-types';
-import React, { Component } from 'react';
-import LazyLoad from 'react-lazyload';
+import React from 'react';
+import ArtistImage from './ArtistImage';
const posterPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPcAAAD3AgMAAAC84irAAAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAAAWJLR0QAiAUdSAAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB+EJEBIzDdm9OfoAAAbkSURBVGje7Zq9b9s4FMBZFgUkBR27C3cw0MromL1jxwyVZASB67G4qWPgoSAyBdm9CwECKCp8nbIccGj/Ce/BTUb3Lh3aI997pCjnTnyyt0JcIif5+ZHvPZLvQ0KMYxzjGMc4xjGOcYxjHOP4JUfSfP7RVPvSH3MYX/eC5aecxne1v+w95WebFs/rwVO/8+h8PnT6t3ln/DFQuJ06/SyHiX9pxa7o5/lewkuLDxLvhM8tPki8g07dU8Gnj5zGlw7P79n4pDVYi8/YuHO4n03z0z6XXDom4G3TXDdN840+LobN/W1Ty2slHD8bNvevlUgutLmTj4NmT3pf6mMGcJGth+gefaZsDCjB2Wj65wN8ZmnAGnE6eFieI1FvcEISLjIUr9hm+w7PFeHiE9t0E7dyIatE48odXTPu0j/A3BMnXf7NXDxudTxbE2VxMWVu+sfwf3i1ZMLiaQLf+iWIP4VtjtTzFhc35vfveZrb4nPt4R95ulu1cxeVh8Psw7rzbgWp8dWHyr83WJpbgjypjS5XeZnqRxmJNUd3MS1d6ue/tOn0WuayNd2CoTlaeqwnIVeOgcWHdHdMS9cSN1vCy3bxZwzFm6VL7QA14WTudVj1sFvf4ReZNSCO0IvwngXFV3hkFcriuPokrPrYbYxjVAHiZ24zLYIeP7/E4xZUgHiZWt29D9ptGemHR7mPo9B10HLGbucRfs/Ww2f2CD4L2u0+wofKwwvrd0XoqCmr38CAZa1d58LesEpvgqtN4MCR1mVj2nZWOiweVB/CAXuyi59Y1auA2eekg6Xw8Tfm013A8LFV8mYXL61ZF4Hb8Zx8d9vBtbdG7s99XvOOZlF38QVtmlkAv0ffxTOjxU/o5p8FvKbSszw2ik87+Iz23Lwf134RiWf2tG3xN2T4oh8vDO4U33z+5qnefFnR77OA2wheh2WfbJBHeI/XgtNJEaHdtJNrvPn8E8eV/kW/2xn8FDc77LemOyq4J1XvSbds7SZ3cAV+86UXP283TGaFUk4ZwmNyugne8FaqxdHtFkH8GNewg2cc3PjsM7CbbNdMwQJ47aL3mP5H308ar5XOn2nUwpx+4hrx/z+qn5DBNqD4rMUpWACnPwnhkfa9SnZwvX1MnHLVi08cPle+0wBuAsykd8dO0KkS9L0dPCO37MVLxJc6nPHdTeNT/ZeLDQN/DEFpBzc33Bfckhx8K1q7IS5vuPgjbTf5AL97zcALxFUHN76QrF7heTHru54RN3bbxTeEn4Xx04f4NOfhSuPLncmnQk3z1yLlSE8fabtFHVyZyIQlXes8zrdSJR5ea7k3+asUooXg2mO4oDprT/XdHpROhouL/8A3edBw5DYxBhYdn08Q53jd0elDfApHbHjL6Hk/pvvNd1rEWdLl9iG+hpMgiMMdVEM64B8X5nq6ZBwX5rCSeK/4uInJROiwetLi0jtpG0yJBPOkTVQXryEPKqMQbq6JeyUTvUOkilq/EVGmo5NIpP3XRIzhXIafrjzF30JUIqecKxIjOpF6il9jbHTLxjs3rN5voPH+GxbDA1m7GrM9a4zdTigdCUUXD2MSSEAXQRxDo2QHl2iwV+h7gchqLrLrhmKxH/Z6nqLUQD5AYSHWAEwk+Z1Ck1vEAmEhBaVtufDtj8Zmv6U+PQNBqbDf/szVR5XNvQteSAzRyeQhzgnIKR2Invq43gQb4+oRaJCTTcRd6RkzGXlJQe3vDq8gsDB2S0QaSoViwKNW9Sh9zUzEMA2MWtU7nJUGYhIa4bnjcLthgkkopMAGj3dxXgoMCbg+laTFL8luSn9pFkrAMf031cmVJz0jXzsKFm6OSfVqYnEILPKZDjeicPFhQoaHbMhKX+NmZ5Q+ntr8n5obhGPVKlx48cs+FteKP3MlswWv6CSPHK4Dmntm0ckreW0snmxKbsnLFdyo4mrwjLYJo+Dmyn0k3uDTEpMRTrnPKza+IHy9wGSEU2yMvSrvHeJ/Qt2UV+p0hVacvsah0psKXqEVy7y2tPu3xhM1oMxLReY00tAlJG9JFZktzCwyU4lbuqQ7U22VN1zi9gvsIP05PjAL7H55H/C6rREzyvu41bbS4VXb1OV0FLG1YVsa1J1gtzaosVJbHO3Gb6z4bR2H89s61FRqCIcgL+E3lfyWlsaN3eR6QDP0pSdeKqOEZjOgoda285SUl5W+Jga181wz0WQFF2poM7FtZTZKXlXZ0Fam10htroY3Ug9s43pN5OJ2jyZy28Iu1nu0sNsGenGzRwO9bd8Xd/u0793LA8Vmn5cHnPhiH+Gt+HIv4Ye+tnHoSyMHvrJy6Aszh76uc+DLQuLQV5XGMY5xjGMc4xjHOMYxjnH80uNfW99BeoyzJCoAAAAASUVORK5CYII=';
-function findPoster(images) {
- return _.find(images, { coverType: 'poster' });
-}
-
-function getPosterUrl(poster, size) {
- if (poster) {
- if (poster.url.contains('lastWrite=') || (/^https?:/).test(poster.url)) {
- // Remove protocol
- let url = poster.url.replace(/^https?:/, '');
- url = url.replace('poster.jpg', `poster-${size}.jpg`);
-
- return url;
- }
- }
-}
-
-class ArtistPoster extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- const pixelRatio = Math.floor(window.devicePixelRatio);
-
- const {
- images,
- size
- } = props;
-
- const poster = findPoster(images);
-
- this.state = {
- pixelRatio,
- poster,
- posterUrl: getPosterUrl(poster, pixelRatio * size),
- isLoaded: false,
- hasError: false
- };
- }
-
- componentDidUpdate(prevProps) {
- const {
- images,
- size
- } = this.props;
-
- const {
- poster,
- pixelRatio
- } = this.state;
-
- const nextPoster = findPoster(images);
-
- if (nextPoster && (!poster || nextPoster.url !== poster.url)) {
- this.setState({
- poster: nextPoster,
- posterUrl: getPosterUrl(nextPoster, pixelRatio * size),
- hasError: false,
- isLoaded: true
- });
- } else if (!nextPoster && poster) {
- this.setState({
- poster: nextPoster,
- posterUrl: posterPlaceholder,
- hasError: false
- });
- }
- }
-
- //
- // Listeners
-
- onError = () => {
- this.setState({ hasError: true });
- }
-
- onLoad = () => {
- this.setState({
- isLoaded: true,
- hasError: false
- });
- }
-
- //
- // Render
-
- render() {
- const {
- className,
- style,
- size,
- lazy,
- overflow
- } = this.props;
-
- const {
- posterUrl,
- hasError,
- isLoaded
- } = this.state;
-
- if (hasError || !posterUrl) {
- return (
-
- );
- }
-
- if (lazy) {
- return (
-
- }
- >
-
-
- );
- }
-
- return (
-
- );
- }
+function ArtistPoster(props) {
+ return (
+
+ );
}
ArtistPoster.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
+ size: PropTypes.number.isRequired
};
ArtistPoster.defaultProps = {
- size: 250,
- lazy: true,
- overflow: false
+ size: 250
};
export default ArtistPoster;
diff --git a/frontend/src/Artist/Details/ArtistDetails.css b/frontend/src/Artist/Details/ArtistDetails.css
index dd1e8be64..6d75e959e 100644
--- a/frontend/src/Artist/Details/ArtistDetails.css
+++ b/frontend/src/Artist/Details/ArtistDetails.css
@@ -108,6 +108,7 @@
}
.details {
+ margin-bottom: 8px;
font-weight: 300;
font-size: 20px;
}
@@ -132,15 +133,11 @@
font-size: 17px;
}
-.path {
- vertical-align: text-top;
- font-size: $defaultFontSize;
- font-family: $monoSpaceFontFamily;
-}
-
.overview {
flex: 1 0 auto;
+ margin-top: 8px;
min-height: 0;
+ font-size: $intermediateFontSize;
}
.contentContainer {
diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js
index 1d2dfe858..316ce692a 100644
--- a/frontend/src/Artist/Details/ArtistDetails.js
+++ b/frontend/src/Artist/Details/ArtistDetails.js
@@ -11,7 +11,6 @@ import HeartRating from 'Components/HeartRating';
import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Label from 'Components/Label';
-import Measure from 'Components/Measure';
import MonitorToggleButton from 'Components/MonitorToggleButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
@@ -35,6 +34,7 @@ import ArtistTagsConnector from './ArtistTagsConnector';
import ArtistDetailsLinks from './ArtistDetailsLinks';
import styles from './ArtistDetails.css';
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
+import ArtistInteractiveSearchModalConnector from 'Artist/Search/ArtistInteractiveSearchModalConnector';
import Link from 'Components/Link/Link';
const defaultFontSize = parseInt(fonts.defaultFontSize);
@@ -71,6 +71,7 @@ class ArtistDetails extends Component {
isDeleteArtistModalOpen: false,
isArtistHistoryModalOpen: false,
isInteractiveImportModalOpen: false,
+ isInteractiveSearchModalOpen: false,
allExpanded: false,
allCollapsed: false,
expandedState: {}
@@ -104,6 +105,14 @@ class ArtistDetails extends Component {
this.setState({ isInteractiveImportModalOpen: false });
}
+ onInteractiveSearchPress = () => {
+ this.setState({ isInteractiveSearchModalOpen: true });
+ }
+
+ onInteractiveSearchModalClose = () => {
+ this.setState({ isInteractiveSearchModalOpen: false });
+ }
+
onEditArtistPress = () => {
this.setState({ isEditArtistModalOpen: true });
}
@@ -181,7 +190,9 @@ class ArtistDetails extends Component {
isPopulated,
albumsError,
trackFilesError,
+ hasAlbums,
hasMonitoredAlbums,
+ hasTrackFiles,
previousArtist,
nextArtist,
onMonitorTogglePress,
@@ -201,6 +212,7 @@ class ArtistDetails extends Component {
isDeleteArtistModalOpen,
isArtistHistoryModalOpen,
isInteractiveImportModalOpen,
+ isInteractiveSearchModalOpen,
allExpanded,
allCollapsed,
expandedState
@@ -240,29 +252,41 @@ class ArtistDetails extends Component {
+
+
@@ -609,6 +633,12 @@ class ArtistDetails extends Component {
showImportMode={false}
onModalClose={this.onInteractiveImportModalClose}
/>
+
+
);
@@ -638,7 +668,9 @@ ArtistDetails.propTypes = {
isPopulated: PropTypes.bool.isRequired,
albumsError: PropTypes.object,
trackFilesError: PropTypes.object,
+ hasAlbums: PropTypes.bool.isRequired,
hasMonitoredAlbums: PropTypes.bool.isRequired,
+ hasTrackFiles: PropTypes.bool.isRequired,
previousArtist: PropTypes.object.isRequired,
nextArtist: PropTypes.object.isRequired,
onMonitorTogglePress: PropTypes.func.isRequired,
diff --git a/frontend/src/Artist/Details/ArtistDetailsConnector.js b/frontend/src/Artist/Details/ArtistDetailsConnector.js
index f7ed9db40..2e5ba1d11 100644
--- a/frontend/src/Artist/Details/ArtistDetailsConnector.js
+++ b/frontend/src/Artist/Details/ArtistDetailsConnector.js
@@ -16,11 +16,55 @@ import { executeCommand } from 'Store/Actions/commandActions';
import * as commandNames from 'Commands/commandNames';
import ArtistDetails from './ArtistDetails';
+const selectAlbums = createSelector(
+ (state) => state.albums,
+ (albums) => {
+ const {
+ items,
+ isFetching,
+ isPopulated,
+ error
+ } = albums;
+
+ const hasAlbums = !!items.length;
+ const hasMonitoredAlbums = items.some((e) => e.monitored);
+
+ return {
+ isAlbumsFetching: isFetching,
+ isAlbumsPopulated: isPopulated,
+ albumsError: error,
+ hasAlbums,
+ hasMonitoredAlbums
+ };
+ }
+);
+
+const selectTrackFiles = createSelector(
+ (state) => state.trackFiles,
+ (trackFiles) => {
+ const {
+ items,
+ isFetching,
+ isPopulated,
+ error
+ } = trackFiles;
+
+ const hasTrackFiles = !!items.length;
+
+ return {
+ isTrackFilesFetching: isFetching,
+ isTrackFilesPopulated: isPopulated,
+ trackFilesError: error,
+ hasTrackFiles
+ };
+ }
+);
+
function createMapStateToProps() {
return createSelector(
(state, { foreignArtistId }) => foreignArtistId,
- (state) => state.albums,
- (state) => state.trackFiles,
+ selectAlbums,
+ selectTrackFiles,
(state) => state.settings.metadataProfiles,
createAllArtistSelector(),
createCommandsSelector(),
@@ -40,6 +84,21 @@ function createMapStateToProps() {
return {};
}
+ const {
+ isAlbumsFetching,
+ isAlbumsPopulated,
+ albumsError,
+ hasAlbums,
+ hasMonitoredAlbums
+ } = albums;
+
+ const {
+ isTrackFilesFetching,
+ isTrackFilesPopulated,
+ trackFilesError,
+ hasTrackFiles
+ } = trackFiles;
+
const sortedAlbumTypes = _.orderBy(albumTypes);
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
@@ -60,10 +119,9 @@ function createMapStateToProps() {
isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1
);
- const isFetching = albums.isFetching || trackFiles.isFetching;
- const isPopulated = albums.isPopulated && trackFiles.isPopulated;
- const albumsError = albums.error;
- const trackFilesError = trackFiles.error;
+ const isFetching = isAlbumsFetching || isTrackFilesFetching;
+ const isPopulated = isAlbumsPopulated && isTrackFilesPopulated;
+
const alternateTitles = _.reduce(artist.alternateTitles, (acc, alternateTitle) => {
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) {
@@ -73,8 +131,6 @@ function createMapStateToProps() {
return acc;
}, []);
- const hasMonitoredAlbums = albums.items.some((e) => e.monitored);
-
return {
...artist,
albumTypes: sortedAlbumTypes,
@@ -89,7 +145,9 @@ function createMapStateToProps() {
isPopulated,
albumsError,
trackFilesError,
+ hasAlbums,
hasMonitoredAlbums,
+ hasTrackFiles,
previousArtist,
nextArtist
};
diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.css b/frontend/src/Artist/Details/ArtistDetailsSeason.css
index 5a0840889..0133e85a2 100644
--- a/frontend/src/Artist/Details/ArtistDetailsSeason.css
+++ b/frontend/src/Artist/Details/ArtistDetailsSeason.css
@@ -62,7 +62,7 @@
composes: menuContent from 'Components/Menu/MenuContent.css';
white-space: nowrap;
- font-size: 14px;
+ font-size: $defaultFontSize;
}
.actionMenuIcon {
diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.js b/frontend/src/Artist/Details/ArtistDetailsSeason.js
index 3df7fca77..f3d105dc0 100644
--- a/frontend/src/Artist/Details/ArtistDetailsSeason.js
+++ b/frontend/src/Artist/Details/ArtistDetailsSeason.js
@@ -34,8 +34,13 @@ class ArtistDetailsSeason extends Component {
}
componentDidUpdate(prevProps) {
- if (prevProps.artistId !== this.props.artistId) {
+ const {
+ artistId
+ } = this.props;
+
+ if (prevProps.artistId !== artistId) {
this._expandByDefault();
+ return;
}
}
@@ -51,7 +56,7 @@ class ArtistDetailsSeason extends Component {
const expand = _.some(items, (item) => {
return isAfter(item.releaseDate) ||
- isAfter(item.releaseDate, { days: -30 });
+ isAfter(item.releaseDate, { days: -365 });
});
onExpandPress(name, expand);
@@ -113,7 +118,6 @@ class ArtistDetailsSeason extends Component {
items,
columns,
isExpanded,
- artistMonitored,
sortKey,
sortDirection,
onSortPress,
@@ -235,7 +239,6 @@ ArtistDetailsSeason.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isExpanded: PropTypes.bool,
- artistMonitored: PropTypes.bool.isRequired,
isSmallScreen: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired,
onExpandPress: PropTypes.func.isRequired,
diff --git a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js
index 11202c7ac..21e25a67d 100644
--- a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js
+++ b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js
@@ -4,14 +4,12 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { findCommand, isCommandExecuting } from 'Utilities/Command';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import { toggleAlbumsMonitored, setAlbumsTableOption, setAlbumsSort } from 'Store/Actions/albumActions';
import { executeCommand } from 'Store/Actions/commandActions';
-import * as commandNames from 'Commands/commandNames';
import ArtistDetailsSeason from './ArtistDetailsSeason';
function createMapStateToProps() {
diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.js b/frontend/src/Artist/Edit/EditArtistModalContent.js
index 4f3fa9803..ba0e657c4 100644
--- a/frontend/src/Artist/Edit/EditArtistModalContent.js
+++ b/frontend/src/Artist/Edit/EditArtistModalContent.js
@@ -85,9 +85,7 @@ class EditArtistModalContent extends Component {
-
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css
index ec7a1dbfd..465f11f91 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexHeader.css
+++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.css
@@ -10,6 +10,20 @@
flex: 4 0 110px;
}
+.banner {
+ flex: 0 0 379px;
+}
+
+.bannerGrow {
+ flex-grow: 1;
+}
+
+.artistType {
+ composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
+
+ flex: 0 0 100px;
+}
+
.qualityProfileId,
.languageProfileId,
.metadataProfileId {
@@ -40,7 +54,6 @@
flex: 0 0 150px;
}
-.artistType,
.trackCount {
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexHeader.js b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js
index f46bbde5f..aed47bafa 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexHeader.js
+++ b/frontend/src/Artist/Index/Table/ArtistIndexHeader.js
@@ -1,109 +1,86 @@
import PropTypes from 'prop-types';
-import React, { Component } from 'react';
+import React from 'react';
+import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import IconButton from 'Components/Link/IconButton';
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
-import TableOptionsModal from 'Components/Table/TableOptions/TableOptionsModal';
+import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
+import hasGrowableColumns from './hasGrowableColumns';
import ArtistIndexTableOptionsConnector from './ArtistIndexTableOptionsConnector';
import styles from './ArtistIndexHeader.css';
-class ArtistIndexHeader extends Component {
-
- //
- // Lifecycle
-
- constructor(props, context) {
- super(props, context);
-
- this.state = {
- isTableOptionsModalOpen: false
- };
- }
-
- //
- // Listeners
-
- onTableOptionsPress = () => {
- this.setState({ isTableOptionsModalOpen: true });
- }
-
- onTableOptionsModalClose = () => {
- this.setState({ isTableOptionsModalOpen: false });
- }
-
- //
- // Render
-
- render() {
- const {
- showSearchAction,
- columns,
- onTableOptionChange,
- ...otherProps
- } = this.props;
-
- return (
-
- {
- columns.map((column) => {
- const {
- name,
- label,
- isSortable,
- isVisible
- } = column;
-
- if (!isVisible) {
- return null;
- }
-
- if (name === 'actions') {
- return (
-
-
-
- );
- }
-
+function ArtistIndexHeader(props) {
+ const {
+ showBanners,
+ columns,
+ onTableOptionChange,
+ ...otherProps
+ } = props;
+
+ return (
+
+ {
+ columns.map((column) => {
+ const {
+ name,
+ label,
+ isSortable,
+ isVisible
+ } = column;
+
+ if (!isVisible) {
+ return null;
+ }
+
+ if (name === 'actions') {
return (
- {label}
+
+
+
+
);
- })
- }
-
-
-
- );
- }
+ }
+
+ return (
+
+ {label}
+
+ );
+ })
+ }
+
+ );
}
ArtistIndexHeader.propTypes = {
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- onTableOptionChange: PropTypes.func.isRequired
+ onTableOptionChange: PropTypes.func.isRequired,
+ showBanners: PropTypes.bool.isRequired
};
export default ArtistIndexHeader;
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.css b/frontend/src/Artist/Index/Table/ArtistIndexRow.css
index 5de727246..83fb0aa0e 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexRow.css
+++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.css
@@ -1,19 +1,69 @@
-.status {
+.cell {
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ display: flex;
+ align-items: center;
+}
+
+.status {
+ composes: cell;
+
flex: 0 0 60px;
}
.sortName {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 4 0 110px;
}
+.artistType {
+ composes: cell;
+
+ flex: 0 0 100px;
+}
+
+.banner {
+ flex: 0 0 379px;
+}
+
+.bannerGrow {
+ flex-grow: 1;
+}
+
+.link {
+ composes: link from 'Components/Link/Link.css';
+
+ position: relative;
+ display: block;
+ height: 70px;
+ background-color: $defaultColor;
+}
+
+.bannerImage {
+ width: 379px;
+ height: 70px;
+}
+
+.overlayTitle {
+ position: absolute;
+ top: 0;
+ left: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 5px;
+ width: 100%;
+ height: 100%;
+ color: $offWhite;
+ text-align: center;
+ font-size: 20px;
+}
+
.qualityProfileId,
.languageProfileId,
.metadataProfileId {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 1 0 125px;
}
@@ -22,19 +72,19 @@
.lastAlbum,
.added,
.genres {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 0 0 180px;
}
.albumCount {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 0 0 100px;
}
.trackProgress {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
display: flex;
justify-content: center;
@@ -42,21 +92,20 @@
flex-direction: column;
}
-.artistType,
.trackCount {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 0 0 130px;
}
.path {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 1 0 150px;
}
.sizeOnDisk {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 0 0 120px;
}
@@ -68,21 +117,21 @@
}
.tags {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 1 0 60px;
}
.useSceneNumbering {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
flex: 0 0 145px;
}
.actions {
- composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
+ composes: cell;
- flex: 0 1 90px;
+ flex: 0 0 90px;
}
.checkInput {
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.js b/frontend/src/Artist/Index/Table/ArtistIndexRow.js
index 787ababb9..b77c499c4 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexRow.js
+++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.js
@@ -1,10 +1,12 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
+import classNames from 'classnames';
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
import formatBytes from 'Utilities/Number/formatBytes';
import { icons } from 'Helpers/Props';
import HeartRating from 'Components/HeartRating';
import IconButton from 'Components/Link/IconButton';
+import Link from 'Components/Link/Link';
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
import ProgressBar from 'Components/ProgressBar';
import TagListConnector from 'Components/TagListConnector';
@@ -16,6 +18,8 @@ import ArtistNameLink from 'Artist/ArtistNameLink';
import AlbumTitleLink from 'Album/AlbumTitleLink';
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
+import ArtistBanner from 'Artist/ArtistBanner';
+import hasGrowableColumns from './hasGrowableColumns';
import ArtistStatusCell from './ArtistStatusCell';
import styles from './ArtistIndexRow.css';
@@ -28,6 +32,7 @@ class ArtistIndexRow extends Component {
super(props, context);
this.state = {
+ hasBannerError: false,
isEditArtistModalOpen: false,
isDeleteArtistModalOpen: false
};
@@ -57,6 +62,18 @@ class ArtistIndexRow extends Component {
//
}
+ onBannerLoad = () => {
+ if (this.state.hasBannerError) {
+ this.setState({ hasBannerError: false });
+ }
+ }
+
+ onBannerLoadError = () => {
+ if (!this.state.hasBannerError) {
+ this.setState({ hasBannerError: true });
+ }
+ }
+
//
// Render
@@ -80,6 +97,8 @@ class ArtistIndexRow extends Component {
ratings,
path,
tags,
+ images,
+ showBanners,
showSearchAction,
columns,
isRefreshingArtist,
@@ -97,6 +116,7 @@ class ArtistIndexRow extends Component {
} = statistics;
const {
+ hasBannerError,
isEditArtistModalOpen,
isDeleteArtistModalOpen
} = this.state;
@@ -130,12 +150,40 @@ class ArtistIndexRow extends Component {
return (
-
+ {
+ showBanners ?
+
+
+
+ {
+ hasBannerError &&
+
+ {artistName}
+
+ }
+ :
+
+
+ }
);
}
@@ -424,6 +472,8 @@ ArtistIndexRow.propTypes = {
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
ratings: PropTypes.object.isRequired,
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
+ images: PropTypes.arrayOf(PropTypes.object).isRequired,
+ showBanners: PropTypes.bool.isRequired,
showSearchAction: PropTypes.bool.isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
isRefreshingArtist: PropTypes.bool.isRequired,
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTable.js b/frontend/src/Artist/Index/Table/ArtistIndexTable.js
index eee92a418..1ed508cc1 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexTable.js
+++ b/frontend/src/Artist/Index/Table/ArtistIndexTable.js
@@ -43,7 +43,8 @@ class ArtistIndexTable extends Component {
rowRenderer = ({ key, rowIndex, style }) => {
const {
items,
- columns
+ columns,
+ showBanners
} = this.props;
const artist = items[rowIndex];
@@ -58,6 +59,7 @@ class ArtistIndexTable extends Component {
languageProfileId={artist.languageProfileId}
qualityProfileId={artist.qualityProfileId}
metadataProfileId={artist.metadataProfileId}
+ showBanners={showBanners}
/>
);
}
@@ -72,6 +74,7 @@ class ArtistIndexTable extends Component {
filters,
sortKey,
sortDirection,
+ showBanners,
isSmallScreen,
scrollTop,
contentBody,
@@ -88,11 +91,12 @@ class ArtistIndexTable extends Component {
scrollIndex={this.state.scrollIndex}
contentBody={contentBody}
isSmallScreen={isSmallScreen}
- rowHeight={38}
+ rowHeight={showBanners ? 70 : 38}
overscanRowCount={2}
rowRenderer={this.rowRenderer}
header={
{
return {
isSmallScreen: dimensions.isSmallScreen,
- ...artist
+ ...artist,
+ showBanners: artist.tableOptions.showBanners
};
}
);
diff --git a/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.js b/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.js
index ce03ba8df..110a024e4 100644
--- a/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.js
+++ b/frontend/src/Artist/Index/Table/ArtistIndexTableOptions.js
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import React, { Component } from 'react';
+import React, { Component, Fragment } from 'react';
import { inputTypes } from 'Helpers/Props';
import FormGroup from 'Components/Form/FormGroup';
import FormLabel from 'Components/Form/FormLabel';
@@ -14,15 +14,23 @@ class ArtistIndexTableOptions extends Component {
super(props, context);
this.state = {
+ showBanners: props.showBanners,
showSearchAction: props.showSearchAction
};
}
componentDidUpdate(prevProps) {
- const { showSearchAction } = this.props;
+ const {
+ showBanners,
+ showSearchAction
+ } = this.props;
- if (showSearchAction !== prevProps.showSearchAction) {
+ if (
+ showBanners !== prevProps.showBanners ||
+ showSearchAction !== prevProps.showSearchAction
+ ) {
this.setState({
+ showBanners,
showSearchAction
});
}
@@ -49,26 +57,42 @@ class ArtistIndexTableOptions extends Component {
render() {
const {
+ showBanners,
showSearchAction
} = this.state;
return (
-
- Show Search
-
-
-
+
+
+ Show Banners
+
+
+
+
+
+ Show Search
+
+
+
+
);
}
}
ArtistIndexTableOptions.propTypes = {
+ showBanners: PropTypes.bool.isRequired,
showSearchAction: PropTypes.bool.isRequired,
onTableOptionChange: PropTypes.func.isRequired
};
diff --git a/frontend/src/Artist/Index/Table/hasGrowableColumns.js b/frontend/src/Artist/Index/Table/hasGrowableColumns.js
new file mode 100644
index 000000000..6af100d1b
--- /dev/null
+++ b/frontend/src/Artist/Index/Table/hasGrowableColumns.js
@@ -0,0 +1,17 @@
+const growableColumns = [
+ 'qualityProfileId',
+ 'languageProfileId',
+ 'path',
+ 'tags'
+];
+
+export default function hasGrowableColumns(columns) {
+ return columns.some((column) => {
+ const {
+ name,
+ isVisible
+ } = column;
+
+ return growableColumns.includes(name) && isVisible;
+ });
+}
diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js
new file mode 100644
index 000000000..0da3661a8
--- /dev/null
+++ b/frontend/src/Artist/Search/ArtistInteractiveSearchModal.js
@@ -0,0 +1,33 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Modal from 'Components/Modal/Modal';
+import ArtistInteractiveSearchModalContent from './ArtistInteractiveSearchModalContent';
+
+function ArtistInteractiveSearchModal(props) {
+ const {
+ isOpen,
+ artistId,
+ onModalClose
+ } = props;
+
+ return (
+
+
+
+ );
+}
+
+ArtistInteractiveSearchModal.propTypes = {
+ isOpen: PropTypes.bool.isRequired,
+ artistId: PropTypes.number.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default ArtistInteractiveSearchModal;
diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModalConnector.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModalConnector.js
new file mode 100644
index 000000000..fe3170570
--- /dev/null
+++ b/frontend/src/Artist/Search/ArtistInteractiveSearchModalConnector.js
@@ -0,0 +1,15 @@
+import { connect } from 'react-redux';
+import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
+import ArtistInteractiveSearchModal from './ArtistInteractiveSearchModal';
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onModalClose() {
+ dispatch(cancelFetchReleases());
+ dispatch(clearReleases());
+ props.onModalClose();
+ }
+ };
+}
+
+export default connect(null, createMapDispatchToProps)(ArtistInteractiveSearchModal);
diff --git a/frontend/src/Artist/Search/ArtistInteractiveSearchModalContent.js b/frontend/src/Artist/Search/ArtistInteractiveSearchModalContent.js
new file mode 100644
index 000000000..9b7f4c6ed
--- /dev/null
+++ b/frontend/src/Artist/Search/ArtistInteractiveSearchModalContent.js
@@ -0,0 +1,45 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Button from 'Components/Link/Button';
+import ModalContent from 'Components/Modal/ModalContent';
+import ModalHeader from 'Components/Modal/ModalHeader';
+import ModalBody from 'Components/Modal/ModalBody';
+import ModalFooter from 'Components/Modal/ModalFooter';
+import InteractiveSearchConnector from 'InteractiveSearch/InteractiveSearchConnector';
+
+function ArtistInteractiveSearchModalContent(props) {
+ const {
+ artistId,
+ onModalClose
+ } = props;
+
+ return (
+
+
+ Interactive Search
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+ArtistInteractiveSearchModalContent.propTypes = {
+ artistId: PropTypes.number.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default ArtistInteractiveSearchModalContent;
diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.css b/frontend/src/Calendar/Agenda/AgendaEvent.css
index d01be1954..bcac8bf72 100644
--- a/frontend/src/Calendar/Agenda/AgendaEvent.css
+++ b/frontend/src/Calendar/Agenda/AgendaEvent.css
@@ -3,15 +3,18 @@
overflow-x: hidden;
padding: 5px;
border-bottom: 1px solid $borderColor;
- font-size: 14px;
+ font-size: $defaultFontSize;
&:hover {
background-color: $tableRowHoverBackgroundColor;
}
}
-.status {
- width: 10px;
+.eventWrapper {
+ display: flex;
+ flex: 1 0 1px;
+ overflow-x: hidden;
+ padding-left: 6px;
border-left-width: 4px;
border-left-style: solid;
}
@@ -24,6 +27,7 @@
.time {
flex: 0 0 120px;
margin-right: 10px;
+ border: none !important;
}
.artistName,
@@ -80,16 +84,16 @@
@media only screen and (max-width: $breakpointSmall) {
.event {
- position: relative;
- flex-wrap: wrap;
- padding-left: 10px;
+ flex-direction: column;
+ }
+
+ .eventWrapper {
+ display: block;
+ flex: 0 0 auto;
}
- .status {
- position: absolute;
- top: 7%;
- left: 0;
- height: 86%;
+ .date {
+ margin-left: 10px;
}
.date,
diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js
index 054206072..f8ce4e0fb 100644
--- a/frontend/src/Calendar/Agenda/AgendaEvent.js
+++ b/frontend/src/Calendar/Agenda/AgendaEvent.js
@@ -49,7 +49,8 @@ class AgendaEvent extends Component {
queueItem,
showDate,
timeFormat,
- longDateFormat
+ longDateFormat,
+ colorImpairedMode
} = this.props;
const startTime = moment(releaseDate);
@@ -74,8 +75,9 @@ class AgendaEvent extends Component {
diff --git a/frontend/src/Calendar/Agenda/AgendaEventConnector.js b/frontend/src/Calendar/Agenda/AgendaEventConnector.js
index 76de94184..b0ab00f1b 100644
--- a/frontend/src/Calendar/Agenda/AgendaEventConnector.js
+++ b/frontend/src/Calendar/Agenda/AgendaEventConnector.js
@@ -15,7 +15,8 @@ function createMapStateToProps() {
artist,
queueItem,
timeFormat: uiSettings.timeFormat,
- longDateFormat: uiSettings.longDateFormat
+ longDateFormat: uiSettings.longDateFormat,
+ colorImpairedMode: uiSettings.enableColorImpairedMode
};
}
);
diff --git a/frontend/src/Calendar/CalendarConnector.js b/frontend/src/Calendar/CalendarConnector.js
index 97d35671b..c5f3e32e6 100644
--- a/frontend/src/Calendar/CalendarConnector.js
+++ b/frontend/src/Calendar/CalendarConnector.js
@@ -41,8 +41,20 @@ class CalendarConnector extends Component {
}
componentDidMount() {
+ const {
+ useCurrentPage,
+ fetchCalendar,
+ gotoCalendarToday
+ } = this.props;
+
registerPagePopulator(this.repopulate);
- this.props.gotoCalendarToday();
+
+ if (useCurrentPage) {
+ fetchCalendar();
+ } else {
+ gotoCalendarToday();
+ }
+
this.scheduleUpdate();
}
diff --git a/frontend/src/Calendar/CalendarPage.js b/frontend/src/Calendar/CalendarPage.js
index d33b1387b..ea795cc52 100644
--- a/frontend/src/Calendar/CalendarPage.js
+++ b/frontend/src/Calendar/CalendarPage.js
@@ -10,7 +10,8 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import FilterMenu from 'Components/Menu/FilterMenu';
import NoArtist from 'Artist/NoArtist';
import CalendarLinkModal from './iCal/CalendarLinkModal';
-import Legend from './Legend/Legend';
+import CalendarOptionsModal from './Options/CalendarOptionsModal';
+import LegendConnector from './Legend/LegendConnector';
import CalendarConnector from './CalendarConnector';
import styles from './CalendarPage.css';
@@ -26,6 +27,7 @@ class CalendarPage extends Component {
this.state = {
isCalendarLinkModalOpen: false,
+ isOptionsModalOpen: false,
width: 0
};
}
@@ -48,6 +50,23 @@ class CalendarPage extends Component {
this.setState({ isCalendarLinkModalOpen: false });
}
+ onOptionsPress = () => {
+ this.setState({ isOptionsModalOpen: true });
+ }
+
+ onOptionsModalClose = () => {
+ this.setState({ isOptionsModalOpen: false });
+ }
+
+ onSearchMissingPress = () => {
+ const {
+ missingAlbumIds,
+ onSearchMissingPress
+ } = this.props;
+
+ onSearchMissingPress(missingAlbumIds);
+ }
+
//
// Render
@@ -56,17 +75,20 @@ class CalendarPage extends Component {
selectedFilterKey,
filters,
hasArtist,
- colorImpairedMode,
+ missingAlbumIds,
+ isSearchingForMissing,
+ useCurrentPage,
onFilterSelect
} = this.props;
- const isMeasured = this.state.width > 0;
+ const {
+ isCalendarLinkModalOpen,
+ isOptionsModalOpen
+ } = this.state;
- let PageComponent = 'div';
+ const isMeasured = this.state.width > 0;
- if (isMeasured) {
- PageComponent = hasArtist ? CalendarConnector : NoArtist;
- }
+ const PageComponent = hasArtist ? CalendarConnector : NoArtist;
return (
@@ -77,9 +99,23 @@ class CalendarPage extends Component {
iconName={icons.CALENDAR}
onPress={this.onGetCalendarLinkPress}
/>
+
+
+
+
-
+ {
+ isMeasured ?
+ :
+
+ }
{
hasArtist &&
-
+
}
+
+
+
);
}
@@ -121,7 +169,10 @@ CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasArtist: PropTypes.bool.isRequired,
- colorImpairedMode: PropTypes.bool.isRequired,
+ missingAlbumIds: PropTypes.arrayOf(PropTypes.number).isRequired,
+ isSearchingForMissing: PropTypes.bool.isRequired,
+ useCurrentPage: PropTypes.bool.isRequired,
+ onSearchMissingPress: PropTypes.func.isRequired,
onDaysCountChange: PropTypes.func.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
diff --git a/frontend/src/Calendar/CalendarPageConnector.js b/frontend/src/Calendar/CalendarPageConnector.js
index d03842628..655275b00 100644
--- a/frontend/src/Calendar/CalendarPageConnector.js
+++ b/frontend/src/Calendar/CalendarPageConnector.js
@@ -1,22 +1,80 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
+import moment from 'moment';
+import { isCommandExecuting } from 'Utilities/Command';
+import isBefore from 'Utilities/Date/isBefore';
+import withCurrentPage from 'Components/withCurrentPage';
+import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
import createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import CalendarPage from './CalendarPage';
+function createMissingAlbumIdsSelector() {
+ return createSelector(
+ (state) => state.calendar.start,
+ (state) => state.calendar.end,
+ (state) => state.calendar.items,
+ (state) => state.queue.details.items,
+ (start, end, albums, queueDetails) => {
+ return albums.reduce((acc, album) => {
+ const releaseDate = album.releaseDate;
+
+ if (
+ album.percentOfTracks < 100 &&
+ moment(releaseDate).isAfter(start) &&
+ moment(releaseDate).isBefore(end) &&
+ isBefore(album.releaseDate) &&
+ !queueDetails.some((details) => !!details.album && details.album.id === album.id)
+ ) {
+ acc.push(album.id);
+ }
+
+ return acc;
+ }, []);
+ }
+ );
+}
+
+function createIsSearchingSelector() {
+ return createSelector(
+ (state) => state.calendar.searchMissingCommandId,
+ createCommandsSelector(),
+ (searchMissingCommandId, commands) => {
+ if (searchMissingCommandId == null) {
+ return false;
+ }
+
+ return isCommandExecuting(commands.find((command) => {
+ return command.id === searchMissingCommandId;
+ }));
+ }
+ );
+}
+
function createMapStateToProps() {
return createSelector(
- (state) => state.calendar,
+ (state) => state.calendar.selectedFilterKey,
+ (state) => state.calendar.filters,
createArtistCountSelector(),
createUISettingsSelector(),
- (calendar, artistCount, uiSettings) => {
+ createMissingAlbumIdsSelector(),
+ createIsSearchingSelector(),
+ (
+ selectedFilterKey,
+ filters,
+ artistCount,
+ uiSettings,
+ missingAlbumIds,
+ isSearchingForMissing
+ ) => {
return {
- selectedFilterKey: calendar.selectedFilterKey,
- filters: calendar.filters,
- showUpcoming: calendar.showUpcoming,
+ selectedFilterKey,
+ filters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
- hasArtist: !!artistCount
+ hasArtist: !!artistCount,
+ missingAlbumIds,
+ isSearchingForMissing
};
}
);
@@ -24,6 +82,9 @@ function createMapStateToProps() {
function createMapDispatchToProps(dispatch, props) {
return {
+ onSearchMissingPress(albumIds) {
+ dispatch(searchMissing({ albumIds }));
+ },
onDaysCountChange(dayCount) {
dispatch(setCalendarDaysCount({ dayCount }));
},
@@ -34,4 +95,6 @@ function createMapDispatchToProps(dispatch, props) {
};
}
-export default connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage);
+export default withCurrentPage(
+ connect(createMapStateToProps, createMapDispatchToProps)(CalendarPage)
+);
diff --git a/frontend/src/Calendar/Events/CalendarEvent.css b/frontend/src/Calendar/Events/CalendarEvent.css
index 03ce086b9..c0153156b 100644
--- a/frontend/src/Calendar/Events/CalendarEvent.css
+++ b/frontend/src/Calendar/Events/CalendarEvent.css
@@ -22,7 +22,7 @@
.artistName {
color: #3a3f51;
- font-size: 14px;
+ font-size: $defaultFontSize;
}
.absoluteEpisodeNumber {
@@ -53,7 +53,7 @@
border-left-color: $gray;
&:global(.colorImpaired) {
- background: repeating-linear-gradient(45deg, transparent, transparent 5px, #eee 5px, #eee 10px);
+ background: repeating-linear-gradient(45deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -61,7 +61,7 @@
border-left-color: $dangerColor;
&:global(.colorImpaired) {
- background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px);
+ background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
@@ -69,6 +69,6 @@
border-left-color: $blue;
&:global(.colorImpaired) {
- background: repeating-linear-gradient(90deg, transparent, transparent 5px, #eee 5px, #eee 10px);
+ background: repeating-linear-gradient(90deg, transparent, transparent 5px, $colorImpairedGradient 5px, $colorImpairedGradient 10px);
}
}
diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js
index b104d5cee..8f04fd670 100644
--- a/frontend/src/Calendar/Events/CalendarEvent.js
+++ b/frontend/src/Calendar/Events/CalendarEvent.js
@@ -4,7 +4,6 @@ import React, { Component } from 'react';
import classNames from 'classnames';
import { icons } from 'Helpers/Props';
import getStatusStyle from 'Calendar/getStatusStyle';
-import albumEntities from 'Album/albumEntities';
import Icon from 'Components/Icon';
import Link from 'Components/Link/Link';
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
diff --git a/frontend/src/Calendar/Legend/Legend.js b/frontend/src/Calendar/Legend/Legend.js
index 8ab94d032..009294930 100644
--- a/frontend/src/Calendar/Legend/Legend.js
+++ b/frontend/src/Calendar/Legend/Legend.js
@@ -1,9 +1,29 @@
import PropTypes from 'prop-types';
import React from 'react';
+import { icons, kinds } from 'Helpers/Props';
import LegendItem from './LegendItem';
+import LegendIconItem from './LegendIconItem';
import styles from './Legend.css';
-function Legend({ colorImpairedMode }) {
+function Legend(props) {
+ const {
+ showCutoffUnmetIcon,
+ colorImpairedMode
+ } = props;
+
+ const iconsToShow = [];
+
+ if (showCutoffUnmetIcon) {
+ iconsToShow.push(
+
+ );
+ }
+
return (
@@ -47,11 +67,24 @@ function Legend({ colorImpairedMode }) {
colorImpairedMode={colorImpairedMode}
/>
+
+
+ {iconsToShow[0]}
+
+
+ {
+ iconsToShow.length > 1 &&
+
+ {iconsToShow[1]}
+ {iconsToShow[2]}
+
+ }
);
}
Legend.propTypes = {
+ showCutoffUnmetIcon: PropTypes.bool.isRequired,
colorImpairedMode: PropTypes.bool.isRequired
};
diff --git a/frontend/src/Calendar/Legend/LegendConnector.js b/frontend/src/Calendar/Legend/LegendConnector.js
new file mode 100644
index 000000000..30bbc4adb
--- /dev/null
+++ b/frontend/src/Calendar/Legend/LegendConnector.js
@@ -0,0 +1,19 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import Legend from './Legend';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar.options,
+ createUISettingsSelector(),
+ (calendarOptions, uiSettings) => {
+ return {
+ ...calendarOptions,
+ colorImpairedMode: uiSettings.enableColorImpairedMode
+ };
+ }
+ );
+}
+
+export default connect(createMapStateToProps)(Legend);
diff --git a/frontend/src/Calendar/Legend/LegendIconItem.css b/frontend/src/Calendar/Legend/LegendIconItem.css
new file mode 100644
index 000000000..01db0ba5a
--- /dev/null
+++ b/frontend/src/Calendar/Legend/LegendIconItem.css
@@ -0,0 +1,10 @@
+.legendIconItem {
+ margin: 3px 0;
+ margin-right: 6px;
+ width: 150px;
+ cursor: default;
+}
+
+.icon {
+ margin-right: 5px;
+}
diff --git a/frontend/src/Calendar/Legend/LegendIconItem.js b/frontend/src/Calendar/Legend/LegendIconItem.js
new file mode 100644
index 000000000..13e106784
--- /dev/null
+++ b/frontend/src/Calendar/Legend/LegendIconItem.js
@@ -0,0 +1,37 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import Icon from 'Components/Icon';
+import styles from './LegendIconItem.css';
+
+function LegendIconItem(props) {
+ const {
+ name,
+ icon,
+ kind,
+ tooltip
+ } = props;
+
+ return (
+
+
+
+ {name}
+
+ );
+}
+
+LegendIconItem.propTypes = {
+ name: PropTypes.string.isRequired,
+ icon: PropTypes.object.isRequired,
+ kind: PropTypes.string.isRequired,
+ tooltip: PropTypes.string.isRequired
+};
+
+export default LegendIconItem;
diff --git a/frontend/src/InteractiveSearch/InteractiveSearchModal.js b/frontend/src/Calendar/Options/CalendarOptionsModal.js
similarity index 54%
rename from frontend/src/InteractiveSearch/InteractiveSearchModal.js
rename to frontend/src/Calendar/Options/CalendarOptionsModal.js
index 7b4b9ffdb..b68c83f30 100644
--- a/frontend/src/InteractiveSearch/InteractiveSearchModal.js
+++ b/frontend/src/Calendar/Options/CalendarOptionsModal.js
@@ -1,13 +1,12 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
-import InteractiveSearchModalContentConnector from './InteractiveSearchModalContentConnector';
+import CalendarOptionsModalContentConnector from './CalendarOptionsModalContentConnector';
-function InteractiveSearchModal(props) {
+function CalendarOptionsModal(props) {
const {
isOpen,
- onModalClose,
- ...otherProps
+ onModalClose
} = props;
return (
@@ -15,17 +14,16 @@ function InteractiveSearchModal(props) {
isOpen={isOpen}
onModalClose={onModalClose}
>
-
);
}
-InteractiveSearchModal.propTypes = {
+CalendarOptionsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
-export default InteractiveSearchModal;
+export default CalendarOptionsModal;
diff --git a/frontend/src/Calendar/Options/CalendarOptionsModalContent.js b/frontend/src/Calendar/Options/CalendarOptionsModalContent.js
new file mode 100644
index 000000000..a25d36f9c
--- /dev/null
+++ b/frontend/src/Calendar/Options/CalendarOptionsModalContent.js
@@ -0,0 +1,216 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { inputTypes } from 'Helpers/Props';
+import FieldSet from 'Components/FieldSet';
+import Button from 'Components/Link/Button';
+import Form from 'Components/Form/Form';
+import FormGroup from 'Components/Form/FormGroup';
+import FormLabel from 'Components/Form/FormLabel';
+import FormInputGroup from 'Components/Form/FormInputGroup';
+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 { firstDayOfWeekOptions, weekColumnOptions, timeFormatOptions } from 'Settings/UI/UISettings';
+
+class CalendarOptionsModalContent extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ const {
+ firstDayOfWeek,
+ calendarWeekColumnHeader,
+ timeFormat,
+ enableColorImpairedMode
+ } = props;
+
+ this.state = {
+ firstDayOfWeek,
+ calendarWeekColumnHeader,
+ timeFormat,
+ enableColorImpairedMode
+ };
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ firstDayOfWeek,
+ calendarWeekColumnHeader,
+ timeFormat,
+ enableColorImpairedMode
+ } = this.props;
+
+ if (
+ prevProps.firstDayOfWeek !== firstDayOfWeek ||
+ prevProps.calendarWeekColumnHeader !== calendarWeekColumnHeader ||
+ prevProps.timeFormat !== timeFormat ||
+ prevProps.enableColorImpairedMode !== enableColorImpairedMode
+ ) {
+ this.setState({
+ firstDayOfWeek,
+ calendarWeekColumnHeader,
+ timeFormat,
+ enableColorImpairedMode
+ });
+ }
+ }
+
+ //
+ // Listeners
+
+ onOptionInputChange = ({ name, value }) => {
+ const {
+ dispatchSetCalendarOption
+ } = this.props;
+
+ dispatchSetCalendarOption({ [name]: value });
+ }
+
+ onGlobalInputChange = ({ name, value }) => {
+ const {
+ dispatchSaveUISettings
+ } = this.props;
+
+ const setting = { [name]: value };
+
+ this.setState(setting, () => {
+ dispatchSaveUISettings(setting);
+ });
+ }
+
+ onLinkFocus = (event) => {
+ event.target.select();
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ collapseMultipleAlbums,
+ showCutoffUnmetIcon,
+ onModalClose
+ } = this.props;
+
+ const {
+ firstDayOfWeek,
+ calendarWeekColumnHeader,
+ timeFormat,
+ enableColorImpairedMode
+ } = this.state;
+
+ return (
+
+
+ Calendar Options
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+CalendarOptionsModalContent.propTypes = {
+ collapseMultipleAlbums: PropTypes.bool.isRequired,
+ showCutoffUnmetIcon: PropTypes.bool.isRequired,
+ firstDayOfWeek: PropTypes.number.isRequired,
+ calendarWeekColumnHeader: PropTypes.string.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ enableColorImpairedMode: PropTypes.bool.isRequired,
+ dispatchSetCalendarOption: PropTypes.func.isRequired,
+ dispatchSaveUISettings: PropTypes.func.isRequired,
+ onModalClose: PropTypes.func.isRequired
+};
+
+export default CalendarOptionsModalContent;
diff --git a/frontend/src/Calendar/Options/CalendarOptionsModalContentConnector.js b/frontend/src/Calendar/Options/CalendarOptionsModalContentConnector.js
new file mode 100644
index 000000000..eb979f74e
--- /dev/null
+++ b/frontend/src/Calendar/Options/CalendarOptionsModalContentConnector.js
@@ -0,0 +1,25 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { setCalendarOption } from 'Store/Actions/calendarActions';
+import CalendarOptionsModalContent from './CalendarOptionsModalContent';
+import { saveUISettings } from 'Store/Actions/settingsActions';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.calendar.options,
+ (state) => state.settings.ui.item,
+ (options, uiSettings) => {
+ return {
+ ...options,
+ ...uiSettings
+ };
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ dispatchSetCalendarOption: setCalendarOption,
+ dispatchSaveUISettings: saveUISettings
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(CalendarOptionsModalContent);
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js
index 758e12691..70c496620 100644
--- a/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValue.js
@@ -19,8 +19,10 @@ function getTagDisplayValue(value, selectedFilterBuilderProp) {
function getValue(input, selectedFilterBuilderProp) {
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
const match = input.match(/^(\d+)([kmgt](i?b)?)$/i);
+
if (match && match.length > 1) {
const [, value, unit] = input.match(/^(\d+)([kmgt](i?b)?)$/i);
+
switch (unit.toLowerCase()) {
case 'k':
return convertToBytes(value, 1, true);
@@ -118,6 +120,7 @@ class FilterBuilderRowValue extends Component {
name: tag && tag.name
};
}
+
return {
id,
name: getTagDisplayValue(id, selectedFilterBuilderProp)
diff --git a/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js b/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js
index ee1dc732e..0290bcdcb 100644
--- a/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js
+++ b/frontend/src/Components/Filter/Builder/QualityFilterBuilderRowValueConnector.js
@@ -12,7 +12,7 @@ function createMapStateToProps() {
(state) => state.settings.qualityProfiles,
(qualityProfiles) => {
const {
- isFetchingSchema: isFetching,
+ isSchemaFetching: isFetching,
isSchemaPopulated: isPopulated,
schemaError: error,
schema
diff --git a/frontend/src/Components/Form/AlbumReleaseSelectInputConnector.js b/frontend/src/Components/Form/AlbumReleaseSelectInputConnector.js
index 862cc1b63..b79c0db1d 100644
--- a/frontend/src/Components/Form/AlbumReleaseSelectInputConnector.js
+++ b/frontend/src/Components/Form/AlbumReleaseSelectInputConnector.js
@@ -44,7 +44,7 @@ class AlbumReleaseSelectInputConnector extends Component {
albumReleases
} = this.props;
- let updatedReleases = _.map(albumReleases.value, (e) => ({ ...e, monitored: false }));
+ const updatedReleases = _.map(albumReleases.value, (e) => ({ ...e, monitored: false }));
_.find(updatedReleases, { foreignReleaseId: value }).monitored = true;
this.props.onChange({ name, value: updatedReleases });
diff --git a/frontend/src/Components/Form/AutoCompleteInput.css b/frontend/src/Components/Form/AutoCompleteInput.css
new file mode 100644
index 000000000..417a71437
--- /dev/null
+++ b/frontend/src/Components/Form/AutoCompleteInput.css
@@ -0,0 +1,58 @@
+.input {
+ composes: input from 'Components/Form/Input.css';
+}
+
+.hasError {
+ composes: hasError from 'Components/Form/Input.css';
+}
+
+.hasWarning {
+ composes: hasWarning from 'Components/Form/Input.css';
+}
+
+.inputWrapper {
+ display: flex;
+}
+
+.inputContainer {
+ position: relative;
+ flex-grow: 1;
+}
+
+.container {
+ @add-mixin scrollbar;
+ @add-mixin scrollbarTrack;
+ @add-mixin scrollbarThumb;
+}
+
+.inputContainerOpen {
+ .container {
+ position: absolute;
+ z-index: 1;
+ overflow-y: auto;
+ max-height: 200px;
+ width: 100%;
+ border: 1px solid $inputBorderColor;
+ border-radius: 4px;
+ background-color: $white;
+ box-shadow: inset 0 1px 1px $inputBoxShadowColor;
+ }
+}
+
+.list {
+ margin: 5px 0;
+ padding-left: 0;
+ list-style-type: none;
+}
+
+.listItem {
+ padding: 0 16px;
+}
+
+.match {
+ font-weight: bold;
+}
+
+.highlighted {
+ background-color: $menuItemHoverBackgroundColor;
+}
diff --git a/frontend/src/Components/Form/AutoCompleteInput.js b/frontend/src/Components/Form/AutoCompleteInput.js
new file mode 100644
index 000000000..740726b36
--- /dev/null
+++ b/frontend/src/Components/Form/AutoCompleteInput.js
@@ -0,0 +1,162 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import Autosuggest from 'react-autosuggest';
+import classNames from 'classnames';
+import jdu from 'jdu';
+import styles from './AutoCompleteInput.css';
+
+class AutoCompleteInput extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ suggestions: []
+ };
+ }
+
+ //
+ // Control
+
+ getSuggestionValue(item) {
+ return item;
+ }
+
+ renderSuggestion(item) {
+ return item;
+ }
+
+ //
+ // Listeners
+
+ onInputChange = (event, { newValue }) => {
+ this.props.onChange({
+ name: this.props.name,
+ value: newValue
+ });
+ }
+
+ onInputKeyDown = (event) => {
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const { suggestions } = this.state;
+
+ if (
+ event.key === 'Tab' &&
+ suggestions.length &&
+ suggestions[0] !== this.props.value
+ ) {
+ event.preventDefault();
+
+ if (value) {
+ onChange({
+ name,
+ value: suggestions[0]
+ });
+ }
+ }
+ }
+
+ onInputBlur = () => {
+ this.setState({ suggestions: [] });
+ }
+
+ onSuggestionsFetchRequested = ({ value }) => {
+ const { values } = this.props;
+ const lowerCaseValue = jdu.replace(value).toLowerCase();
+
+ const filteredValues = values.filter((v) => {
+ return jdu.replace(v).toLowerCase().contains(lowerCaseValue);
+ });
+
+ this.setState({ suggestions: filteredValues });
+ }
+
+ onSuggestionsClearRequested = () => {
+ this.setState({ suggestions: [] });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ className,
+ inputClassName,
+ name,
+ value,
+ placeholder,
+ hasError,
+ hasWarning
+ } = this.props;
+
+ const { suggestions } = this.state;
+
+ const inputProps = {
+ className: classNames(
+ inputClassName,
+ hasError && styles.hasError,
+ hasWarning && styles.hasWarning,
+ ),
+ name,
+ value,
+ placeholder,
+ autoComplete: 'off',
+ spellCheck: false,
+ onChange: this.onInputChange,
+ onKeyDown: this.onInputKeyDown,
+ onBlur: this.onInputBlur
+ };
+
+ const theme = {
+ container: styles.inputContainer,
+ containerOpen: styles.inputContainerOpen,
+ suggestionsContainer: styles.container,
+ suggestionsList: styles.list,
+ suggestion: styles.listItem,
+ suggestionHighlighted: styles.highlighted
+ };
+
+ return (
+
+ );
+ }
+}
+
+AutoCompleteInput.propTypes = {
+ className: PropTypes.string.isRequired,
+ inputClassName: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ value: PropTypes.string,
+ values: PropTypes.arrayOf(PropTypes.string).isRequired,
+ placeholder: PropTypes.string,
+ hasError: PropTypes.bool,
+ hasWarning: PropTypes.bool,
+ onChange: PropTypes.func.isRequired
+};
+
+AutoCompleteInput.defaultProps = {
+ className: styles.inputWrapper,
+ inputClassName: styles.input,
+ value: ''
+};
+
+export default AutoCompleteInput;
diff --git a/frontend/src/Components/Form/Form.css b/frontend/src/Components/Form/Form.css
new file mode 100644
index 000000000..52e79aec4
--- /dev/null
+++ b/frontend/src/Components/Form/Form.css
@@ -0,0 +1,3 @@
+.validationFailures {
+ margin-bottom: 20px;
+}
diff --git a/frontend/src/Components/Form/Form.js b/frontend/src/Components/Form/Form.js
index 9a605297a..c2c67eddf 100644
--- a/frontend/src/Components/Form/Form.js
+++ b/frontend/src/Components/Form/Form.js
@@ -2,37 +2,42 @@ import PropTypes from 'prop-types';
import React from 'react';
import { kinds } from 'Helpers/Props';
import Alert from 'Components/Alert';
+import styles from './Form.css';
function Form({ children, validationErrors, validationWarnings, ...otherProps }) {
return (
-
- {
- validationErrors.map((error, index) => {
- return (
-
- {error.errorMessage}
-
- );
- })
- }
+ {
+ validationErrors.length || validationWarnings.length ?
+
+ {
+ validationErrors.map((error, index) => {
+ return (
+
+ {error.errorMessage}
+
+ );
+ })
+ }
- {
- validationWarnings.map((warning, index) => {
- return (
-
- {warning.errorMessage}
-
- );
- })
- }
-
+ {
+ validationWarnings.map((warning, index) => {
+ return (
+
+ {warning.errorMessage}
+
+ );
+ })
+ }
+
:
+ null
+ }
{children}
diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js
index 2a38be6b1..a487d1a0b 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -2,9 +2,11 @@ import PropTypes from 'prop-types';
import React from 'react';
import { inputTypes } from 'Helpers/Props';
import Link from 'Components/Link/Link';
+import AutoCompleteInput from './AutoCompleteInput';
import CaptchaInputConnector from './CaptchaInputConnector';
import CheckInput from './CheckInput';
import DeviceInputConnector from './DeviceInputConnector';
+import KeyValueListInput from './KeyValueListInput';
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
import NumberInput from './NumberInput';
import OAuthInputConnector from './OAuthInputConnector';
@@ -25,6 +27,9 @@ import styles from './FormInputGroup.css';
function getComponent(type) {
switch (type) {
+ case inputTypes.AUTO_COMPLETE:
+ return AutoCompleteInput;
+
case inputTypes.CAPTCHA:
return CaptchaInputConnector;
@@ -34,6 +39,9 @@ function getComponent(type) {
case inputTypes.DEVICE:
return DeviceInputConnector;
+ case inputTypes.KEY_VALUE_LIST:
+ return KeyValueListInput;
+
case inputTypes.MONITOR_ALBUMS_SELECT:
return MonitorAlbumsSelectInput;
diff --git a/frontend/src/Components/Form/KeyValueListInput.css b/frontend/src/Components/Form/KeyValueListInput.css
new file mode 100644
index 000000000..59be3a4d7
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInput.css
@@ -0,0 +1,21 @@
+.inputContainer {
+ composes: input from 'Components/Form/Input.css';
+
+ position: relative;
+ min-height: 35px;
+ height: auto;
+
+ &.isFocused {
+ outline: 0;
+ border-color: $inputFocusBorderColor;
+ box-shadow: inset 0 1px 1px $inputBoxShadowColor, 0 0 8px $inputFocusBoxShadowColor;
+ }
+}
+
+.hasError {
+ composes: hasError from 'Components/Form/Input.css';
+}
+
+.hasWarning {
+ composes: hasWarning from 'Components/Form/Input.css';
+}
diff --git a/frontend/src/Components/Form/KeyValueListInput.js b/frontend/src/Components/Form/KeyValueListInput.js
new file mode 100644
index 000000000..a52c76f70
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInput.js
@@ -0,0 +1,152 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import classNames from 'classnames';
+import KeyValueListInputItem from './KeyValueListInputItem';
+import styles from './KeyValueListInput.css';
+
+class KeyValueListInput extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ isFocused: false
+ };
+ }
+
+ //
+ // Listeners
+
+ onItemChange = (index, itemValue) => {
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const newValue = [...value];
+
+ if (index == null) {
+ newValue.push(itemValue);
+ } else {
+ newValue.splice(index, 1, itemValue);
+ }
+
+ onChange({
+ name,
+ value: newValue
+ });
+ }
+
+ onRemoveItem = (index) => {
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const newValue = [...value];
+ newValue.splice(index, 1);
+
+ onChange({
+ name,
+ value: newValue
+ });
+ }
+
+ onFocus = () => {
+ this.setState({
+ isFocused: true
+ });
+ }
+
+ onBlur = () => {
+ this.setState({
+ isFocused: false
+ });
+
+ const {
+ name,
+ value,
+ onChange
+ } = this.props;
+
+ const newValue = value.reduce((acc, v) => {
+ if (v.key || v.value) {
+ acc.push(v);
+ }
+
+ return acc;
+ }, []);
+
+ if (newValue.length !== value.length) {
+ onChange({
+ name,
+ value: newValue
+ });
+ }
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ className,
+ value,
+ keyPlaceholder,
+ valuePlaceholder
+ } = this.props;
+
+ const { isFocused } = this.state;
+
+ return (
+
+ {
+ [...value, { key: '', value: '' }].map((v, index) => {
+ return (
+
+ );
+ })
+ }
+
+ );
+ }
+}
+
+KeyValueListInput.propTypes = {
+ className: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ value: PropTypes.arrayOf(PropTypes.object).isRequired,
+ hasError: PropTypes.bool,
+ hasWarning: PropTypes.bool,
+ keyPlaceholder: PropTypes.string,
+ valuePlaceholder: PropTypes.string,
+ onChange: PropTypes.func.isRequired
+};
+
+KeyValueListInput.defaultProps = {
+ className: styles.inputContainer,
+ value: []
+};
+
+export default KeyValueListInput;
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.css b/frontend/src/Components/Form/KeyValueListInputItem.css
new file mode 100644
index 000000000..f77ea3470
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInputItem.css
@@ -0,0 +1,14 @@
+.itemContainer {
+ display: flex;
+ margin-bottom: 3px;
+ border-bottom: 1px solid $inputBorderColor;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+}
+
+.keyInput,
+.valueInput {
+ border: none;
+}
diff --git a/frontend/src/Components/Form/KeyValueListInputItem.js b/frontend/src/Components/Form/KeyValueListInputItem.js
new file mode 100644
index 000000000..4e465f3a9
--- /dev/null
+++ b/frontend/src/Components/Form/KeyValueListInputItem.js
@@ -0,0 +1,117 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { icons } from 'Helpers/Props';
+import IconButton from 'Components/Link/IconButton';
+import TextInput from './TextInput';
+import styles from './KeyValueListInputItem.css';
+
+class KeyValueListInputItem extends Component {
+
+ //
+ // Listeners
+
+ onKeyChange = ({ value: keyValue }) => {
+ const {
+ index,
+ value,
+ onChange
+ } = this.props;
+
+ onChange(index, { key: keyValue, value });
+ }
+
+ onValueChange = ({ value }) => {
+ // TODO: Validate here or validate at a lower level component
+
+ const {
+ index,
+ keyValue,
+ onChange
+ } = this.props;
+
+ onChange(index, { key: keyValue, value });
+ }
+
+ onRemovePress = () => {
+ const {
+ index,
+ onRemove
+ } = this.props;
+
+ onRemove(index);
+ }
+
+ onFocus = () => {
+ this.props.onFocus();
+ }
+
+ onBlur = () => {
+ this.props.onBlur();
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ keyValue,
+ value,
+ keyPlaceholder,
+ valuePlaceholder,
+ isNew
+ } = this.props;
+
+ return (
+
+
+
+
+
+ {
+ !isNew &&
+
+ }
+
+ );
+ }
+}
+
+KeyValueListInputItem.propTypes = {
+ index: PropTypes.number,
+ keyValue: PropTypes.string.isRequired,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ keyPlaceholder: PropTypes.string.isRequired,
+ valuePlaceholder: PropTypes.string.isRequired,
+ isNew: PropTypes.bool.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onRemove: PropTypes.func.isRequired,
+ onFocus: PropTypes.func.isRequired,
+ onBlur: PropTypes.func.isRequired
+};
+
+KeyValueListInputItem.defaultProps = {
+ keyPlaceholder: 'Key',
+ valuePlaceholder: 'Value'
+};
+
+export default KeyValueListInputItem;
diff --git a/frontend/src/Components/Form/MonitorAlbumsSelectInput.js b/frontend/src/Components/Form/MonitorAlbumsSelectInput.js
index 71f39a146..a3780de56 100644
--- a/frontend/src/Components/Form/MonitorAlbumsSelectInput.js
+++ b/frontend/src/Components/Form/MonitorAlbumsSelectInput.js
@@ -1,17 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
+import monitorOptions from 'Utilities/Artist/monitorOptions';
import SelectInput from './SelectInput';
-const monitorOptions = [
- { key: 'all', value: 'All Albums' },
- { key: 'future', value: 'Future Albums' },
- { key: 'missing', value: 'Missing Albums' },
- { key: 'existing', value: 'Existing Albums' },
- { key: 'first', value: 'Only First Album' },
- { key: 'latest', value: 'Only Latest Album' },
- { key: 'none', value: 'None' }
-];
-
function MonitorAlbumsSelectInput(props) {
const {
includeNoChange,
diff --git a/frontend/src/Components/Form/NumberInput.js b/frontend/src/Components/Form/NumberInput.js
index 20b6fd0a1..c4ecc7e86 100644
--- a/frontend/src/Components/Form/NumberInput.js
+++ b/frontend/src/Components/Form/NumberInput.js
@@ -2,44 +2,91 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import TextInput from './TextInput';
+function parseValue(props, value) {
+ const {
+ isFloat,
+ min,
+ max
+ } = props;
+
+ if (value == null || value === '') {
+ return min;
+ }
+
+ let newValue = isFloat ? parseFloat(value) : parseInt(value);
+
+ if (min != null && newValue != null && newValue < min) {
+ newValue = min;
+ } else if (max != null && newValue != null && newValue > max) {
+ newValue = max;
+ }
+
+ return newValue;
+}
+
class NumberInput extends Component {
//
- // Listeners
+ // Lifecycle
- onChange = ({ name, value }) => {
- let newValue = null;
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ value: props.value == null ? '' : props.value.toString(),
+ isFocused: false
+ };
+ }
- if (value) {
- newValue = this.props.isFloat ? parseFloat(value) : parseInt(value);
+ componentDidUpdate(prevProps, prevState) {
+ const { value } = this.props;
+
+ if (value !== prevProps.value && !this.state.isFocused) {
+ this.setState({
+ value: value == null ? '' : value.toString()
+ });
}
+ }
+
+ //
+ // Listeners
+
+ onChange = ({ name, value }) => {
+ this.setState({ value });
this.props.onChange({
name,
- value: newValue
+ value: parseValue(this.props, value)
});
+
+ }
+
+ onFocus = () => {
+ this.setState({ isFocused: true });
}
onBlur = () => {
const {
name,
- value,
- min,
- max,
onChange
} = this.props;
- let newValue = value;
+ const { value } = this.state;
+ const parsedValue = parseValue(this.props, value);
+ const stringValue = parsedValue == null ? '' : parsedValue.toString();
- if (min != null && newValue != null && newValue < min) {
- newValue = min;
- } else if (max != null && newValue != null && newValue > max) {
- newValue = max;
+ if (stringValue === value) {
+ this.setState({ isFocused: false });
+ } else {
+ this.setState({
+ value: stringValue,
+ isFocused: false
+ });
}
onChange({
name,
- value: newValue
+ value: parsedValue
});
}
@@ -47,18 +94,16 @@ class NumberInput extends Component {
// Render
render() {
- const {
- value,
- ...otherProps
- } = this.props;
+ const value = this.state.value;
return (
);
}
diff --git a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css
index d1fdcb08e..0a8fa6ffe 100644
--- a/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css
+++ b/frontend/src/Components/Form/RootFolderSelectInputSelectedValue.css
@@ -14,9 +14,7 @@
}
.freeSpace {
- @add-mixin truncate;
-
- flex: 1 0 0;
+ flex: 0 0 auto;
margin-left: 15px;
color: $gray;
text-align: right;
diff --git a/frontend/src/Components/Form/TagInputTag.js b/frontend/src/Components/Form/TagInputTag.js
index 8cb5486bc..ff1e0e2db 100644
--- a/frontend/src/Components/Form/TagInputTag.js
+++ b/frontend/src/Components/Form/TagInputTag.js
@@ -33,7 +33,10 @@ class TagInputTag extends Component {
} = this.props;
return (
-
+
diff --git a/frontend/src/Components/Form/TextInput.js b/frontend/src/Components/Form/TextInput.js
index 92c0f4baf..9feefa616 100644
--- a/frontend/src/Components/Form/TextInput.js
+++ b/frontend/src/Components/Form/TextInput.js
@@ -127,6 +127,7 @@ class TextInput extends Component {
hasError,
hasWarning,
hasButton,
+ step,
onBlur
} = this.props;
@@ -146,6 +147,7 @@ class TextInput extends Component {
)}
name={name}
value={value}
+ step={step}
onChange={this.onChange}
onFocus={this.onFocus}
onBlur={onBlur}
@@ -168,6 +170,7 @@ TextInput.propTypes = {
hasError: PropTypes.bool,
hasWarning: PropTypes.bool,
hasButton: PropTypes.bool,
+ step: PropTypes.number,
onChange: PropTypes.func.isRequired,
onFocus: PropTypes.func,
onBlur: PropTypes.func,
diff --git a/frontend/src/Components/Icon.css b/frontend/src/Components/Icon.css
index 8c6c10d0f..df1ff5327 100644
--- a/frontend/src/Components/Icon.css
+++ b/frontend/src/Components/Icon.css
@@ -6,10 +6,18 @@
color: inherit;
}
+.disabled {
+ color: $disabledColor;
+}
+
.info {
color: $infoColor;
}
+.pink {
+ color: $pink;
+}
+
.success {
color: $successColor;
}
diff --git a/frontend/src/Components/Label.css b/frontend/src/Components/Label.css
index b63b760c2..df17427d9 100644
--- a/frontend/src/Components/Label.css
+++ b/frontend/src/Components/Label.css
@@ -30,6 +30,15 @@
}
}
+.disabled {
+ border-color: $disabledColor;
+ background-color: $disabledColor;
+
+ &.outline {
+ color: $disabledColor;
+ }
+}
+
.info {
border-color: $infoColor;
background-color: $infoColor;
@@ -92,7 +101,7 @@
.large {
padding: 3px 7px;
font-weight: bold;
- font-size: 14px;
+ font-size: $defaultFontSize;
}
/** Outline **/
diff --git a/frontend/src/Components/Link/IconButton.js b/frontend/src/Components/Link/IconButton.js
index 084e57878..26aacb0bf 100644
--- a/frontend/src/Components/Link/IconButton.js
+++ b/frontend/src/Components/Link/IconButton.js
@@ -24,7 +24,6 @@ function IconButton(props) {
isDisabled && styles.isDisabled
)}
isDisabled={isDisabled}
-
{...otherProps}
>
{
- if (!newValue) {
+ onChange = (event, { newValue, method }) => {
+ if (method === 'up' || method === 'down') {
return;
}
@@ -117,6 +117,7 @@ class ArtistSearchInput extends Component {
if (!suggestions.length || highlightedSectionIndex && (event.key !== 'ArrowDown' || event.key !== 'ArrowUp')) {
this.props.onGoToAddNewArtist(value);
this._autosuggest.input.blur();
+ this.reset();
return;
}
@@ -129,6 +130,9 @@ class ArtistSearchInput extends Component {
} else {
this.goToArtist(suggestions[highlightedSuggestionIndex]);
}
+
+ this._autosuggest.input.blur();
+ this.reset();
}
onBlur = () => {
@@ -142,9 +146,15 @@ class ArtistSearchInput extends Component {
// Check the title first and if there isn't a match fallback to
// the alternate titles and finally the tags.
+ if (value.length === 1) {
+ return (
+ artist.cleanName.startsWith(lowerCaseValue) ||
+ artist.tags.some((tag) => tag.cleanLabel.startsWith(lowerCaseValue))
+ );
+ }
+
return (
artist.cleanName.contains(lowerCaseValue) ||
- // artist.alternateTitles.some((alternateTitle) => alternateTitle.cleanTitle.contains(lowerCaseValue)) ||
artist.tags.some((tag) => tag.cleanLabel.contains(lowerCaseValue))
);
});
@@ -153,7 +163,9 @@ class ArtistSearchInput extends Component {
}
onSuggestionsClearRequested = () => {
- this.reset();
+ this.setState({
+ suggestions: []
+ });
}
onSuggestionSelected = (event, { suggestion }) => {
diff --git a/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js
index 44cc6b2cd..55f1d9a25 100644
--- a/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js
+++ b/frontend/src/Components/Page/Header/ArtistSearchInputConnector.js
@@ -58,10 +58,10 @@ function createCleanArtistSelector() {
})
};
}).sort((a, b) => {
- if (a.cleanName < b.cleanName) {
+ if (a.sortName < b.sortName) {
return -1;
}
- if (a.cleanName > b.cleanName) {
+ if (a.sortName > b.sortName) {
return 1;
}
diff --git a/frontend/src/Components/Page/Header/PageHeader.css b/frontend/src/Components/Page/Header/PageHeader.css
index 3bfcbc10b..1974cbcb1 100644
--- a/frontend/src/Components/Page/Header/PageHeader.css
+++ b/frontend/src/Components/Page/Header/PageHeader.css
@@ -4,14 +4,19 @@
align-items: center;
flex: 0 0 auto;
height: $headerHeight;
- background-color: #00a65b;
+ background-color: $themeAlternateBlue;
color: $white;
}
.logoContainer {
display: flex;
- justify-content: center;
+ align-items: center;
flex: 0 0 $sidebarWidth;
+ padding-left: 20px;
+}
+
+.logoLink {
+ line-height: 0;
}
.logo {
diff --git a/frontend/src/Components/Page/Header/PageHeader.js b/frontend/src/Components/Page/Header/PageHeader.js
index add4e1939..87cf317b3 100644
--- a/frontend/src/Components/Page/Header/PageHeader.js
+++ b/frontend/src/Components/Page/Header/PageHeader.js
@@ -51,7 +51,10 @@ class PageHeader extends Component {
return (
-
+
-
-
-
-
-
+
+
+
+
+
);
}
}
@@ -118,6 +122,7 @@ Page.propTypes = {
isSidebarVisible: PropTypes.bool.isRequired,
isUpdated: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired,
+ enableColorImpairedMode: PropTypes.bool.isRequired,
onResize: PropTypes.func.isRequired,
onSidebarToggle: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
diff --git a/frontend/src/Components/Page/PageConnector.js b/frontend/src/Components/Page/PageConnector.js
index bdb00ccec..4fd993002 100644
--- a/frontend/src/Components/Page/PageConnector.js
+++ b/frontend/src/Components/Page/PageConnector.js
@@ -1,4 +1,3 @@
-/* eslint max-params: 0 */
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
@@ -33,30 +32,45 @@ function createMapStateToProps() {
(state) => state.artist,
(state) => state.customFilters,
(state) => state.tags,
- (state) => state.settings,
+ (state) => state.settings.ui,
+ (state) => state.settings.qualityProfiles,
+ (state) => state.settings.languageProfiles,
+ (state) => state.settings.metadataProfiles,
+ (state) => state.settings.importLists,
(state) => state.app,
createDimensionsSelector(),
- (artist, customFilters, tags, settings, app, dimensions) => {
+ (
+ artist,
+ customFilters,
+ tags,
+ uiSettings,
+ qualityProfiles,
+ languageProfiles,
+ metadataProfiles,
+ importLists,
+ app,
+ dimensions
+ ) => {
const isPopulated = (
artist.isPopulated &&
customFilters.isPopulated &&
tags.isPopulated &&
- settings.qualityProfiles.isPopulated &&
- settings.languageProfiles.isPopulated &&
- settings.metadataProfiles.isPopulated &&
- settings.importLists.isPopulated &&
- settings.ui.isPopulated
+ qualityProfiles.isPopulated &&
+ languageProfiles.isPopulated &&
+ metadataProfiles.isPopulated &&
+ importLists.isPopulated &&
+ uiSettings.isPopulated
);
const hasError = !!(
artist.error ||
customFilters.error ||
tags.error ||
- settings.qualityProfiles.error ||
- settings.languageProfiles.error ||
- settings.metadataProfiles.error ||
- settings.importLists.error ||
- settings.ui.error
+ qualityProfiles.error ||
+ languageProfiles.error ||
+ metadataProfiles.error ||
+ importLists.error ||
+ uiSettings.error
);
return {
@@ -65,13 +79,14 @@ function createMapStateToProps() {
artistError: artist.error,
customFiltersError: tags.error,
tagsError: tags.error,
- qualityProfilesError: settings.qualityProfiles.error,
- languageProfilesError: settings.languageProfiles.error,
- metadataProfilesError: settings.metadataProfiles.error,
- importListsError: settings.importLists.error,
- uiSettingsError: settings.ui.error,
+ qualityProfilesError: qualityProfiles.error,
+ languageProfilesError: languageProfiles.error,
+ metadataProfilesError: metadataProfiles.error,
+ importListsError: importLists.error,
+ uiSettingsError: uiSettings.error,
isSmallScreen: dimensions.isSmallScreen,
isSidebarVisible: app.isSidebarVisible,
+ enableColorImpairedMode: uiSettings.item.enableColorImpairedMode,
version: app.version,
isUpdated: app.isUpdated,
isDisconnected: app.isDisconnected
diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.css b/frontend/src/Components/Page/Toolbar/PageToolbarSection.css
index 5fb56b77c..638636ffb 100644
--- a/frontend/src/Components/Page/Toolbar/PageToolbarSection.css
+++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.css
@@ -22,6 +22,19 @@
justify-content: flex-end;
}
+.overflowMenuButton {
+ composes: menuButton from 'Components/Menu/ToolbarMenuButton.css';
+}
+
.overflowMenuItemIcon {
margin-right: 8px;
}
+
+@media only screen and (max-width: $breakpointSmall) {
+ .overflowMenuButton {
+ &::after {
+ margin-left: 0;
+ content: '\25BE';
+ }
+ }
+}
diff --git a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js
index 57b53ff4e..35ee586ec 100644
--- a/frontend/src/Components/Page/Toolbar/PageToolbarSection.js
+++ b/frontend/src/Components/Page/Toolbar/PageToolbarSection.js
@@ -160,6 +160,7 @@ class PageToolbarSection extends Component {
!!overflowItems.length &&