mirror of https://github.com/lidarr/Lidarr
New: Release Profiles, Frontend updates (#580)
* New: Release Profiles - UI Updates * New: Release Profiles - API Changes * New: Release Profiles - Test Updates * New: Release Profiles - Backend Updates * New: Interactive Artist Search * New: Change Montiored on Album Details Page * New: Show Duration on Album Details Page * Fixed: Manual Import not working if no albums are Missing * Fixed: Sort search input by sortTitle * Fixed: Queue columnLabel throwing JS error
This commit is contained in:
parent
f126eafd26
commit
3f064c94b9
|
@ -177,7 +177,7 @@
|
|||
"no-undef": "error",
|
||||
"no-undef-init": "off",
|
||||
"no-undefined": "off",
|
||||
"no-unused-vars": ["warn", { "args": "none" }],
|
||||
"no-unused-vars": ["error", { "args": "none", "ignoreRestSiblings": true }],
|
||||
"no-use-before-define": "error",
|
||||
|
||||
# Node.js and CommonJS
|
||||
|
@ -205,14 +205,13 @@
|
|||
"func-style": ["error", "declaration"],
|
||||
"indent": ["error", 2, {"SwitchCase": 1}],
|
||||
"key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
|
||||
"keyword-spacing": ["error", {before: true, after: true}],
|
||||
"keyword-spacing": ["error", { "before": true, "after": true}],
|
||||
"lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }],
|
||||
"max-depth": ["error", {"maximum": 5}],
|
||||
"max-nested-callbacks": ["error", 4],
|
||||
"max-params": ["error", 7],
|
||||
"max-statements": "off",
|
||||
"max-statements-per-line": ["error", { "max": 1 }],
|
||||
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred"]}],
|
||||
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred", "DragDropContext", "DragLayer", "DragSource", "DropTarget"]}],
|
||||
"new-parens": "error",
|
||||
"newline-after-var": "off",
|
||||
"newline-before-return": "off",
|
||||
|
@ -223,7 +222,7 @@
|
|||
"no-inline-comments": "off",
|
||||
"no-lonely-if": "warn",
|
||||
"no-mixed-spaces-and-tabs": "error",
|
||||
"no-multiple-empty-lines": ["error", {max: 1}],
|
||||
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||
"no-negated-condition": "warn",
|
||||
"no-nested-ternary": "error",
|
||||
"no-new-object": "error",
|
||||
|
|
|
@ -14,6 +14,7 @@ module.exports = (ctx, configPath, options) => {
|
|||
return Object.assign(acc, reload(vars));
|
||||
}, {})
|
||||
},
|
||||
'postcss-color-function': {},
|
||||
'postcss-nested': {},
|
||||
autoprefixer: {
|
||||
browsers: [
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
|
@ -41,6 +42,18 @@ class Blacklist extends Component {
|
|||
onPress={onClearBlacklistPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
|
@ -33,8 +34,19 @@ class BlacklistConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchBlacklist,
|
||||
gotoBlacklistFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchBlacklist();
|
||||
} else {
|
||||
gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -44,6 +56,7 @@ class BlacklistConnector extends Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearBlacklist();
|
||||
unregisterPagePopulator(this.repopulate);
|
||||
}
|
||||
|
||||
|
@ -53,7 +66,6 @@ class BlacklistConnector extends Component {
|
|||
repopulate = () => {
|
||||
this.props.fetchBlacklist();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
|
@ -93,6 +105,14 @@ class BlacklistConnector extends Component {
|
|||
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||
}
|
||||
|
||||
onTableOptionChange = (payload) => {
|
||||
this.props.setBlacklistTableOption(payload);
|
||||
|
||||
if (payload.pageSize) {
|
||||
this.props.gotoBlacklistFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -114,6 +134,7 @@ class BlacklistConnector extends Component {
|
|||
}
|
||||
|
||||
BlacklistConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchBlacklist: PropTypes.func.isRequired,
|
||||
|
@ -124,7 +145,10 @@ BlacklistConnector.propTypes = {
|
|||
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||
setBlacklistSort: PropTypes.func.isRequired,
|
||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||
clearBlacklist: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector);
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(BlacklistConnector)
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import IconButton from 'Components/Link/IconButton';
|
|||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import TrackLanguage from 'Album/TrackLanguage';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||
|
@ -90,7 +90,7 @@ class BlacklistRow extends Component {
|
|||
key={name}
|
||||
className={styles.language}
|
||||
>
|
||||
<EpisodeLanguage
|
||||
<TrackLanguage
|
||||
language={language}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
.description {
|
||||
composes: title from 'Components/DescriptionList/DescriptionListItemDescription.css';
|
||||
|
||||
overflow-wrap: break-word;
|
||||
}
|
|
@ -7,6 +7,7 @@ import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
function getDetailedList(statusMessages) {
|
||||
return (
|
||||
|
@ -60,6 +61,7 @@ function HistoryDetails(props) {
|
|||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
@ -75,6 +77,7 @@ function HistoryDetails(props) {
|
|||
{
|
||||
!!releaseGroup &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Release Group"
|
||||
data={releaseGroup}
|
||||
/>
|
||||
|
@ -136,6 +139,7 @@ function HistoryDetails(props) {
|
|||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
@ -160,6 +164,7 @@ function HistoryDetails(props) {
|
|||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
@ -167,6 +172,7 @@ function HistoryDetails(props) {
|
|||
{
|
||||
!!droppedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Source"
|
||||
data={droppedPath}
|
||||
/>
|
||||
|
@ -175,6 +181,7 @@ function HistoryDetails(props) {
|
|||
{
|
||||
!!importedPath &&
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Imported To"
|
||||
data={importedPath}
|
||||
/>
|
||||
|
|
|
@ -5,6 +5,7 @@ import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
|||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
|
@ -75,6 +76,16 @@ class History extends Component {
|
|||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
|
|
|
@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
|
|||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
|
||||
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
||||
|
@ -43,8 +44,19 @@ class HistoryConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchHistory,
|
||||
gotoHistoryFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
this.props.gotoHistoryFirstPage();
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchHistory();
|
||||
} else {
|
||||
gotoHistoryFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
|
@ -138,6 +150,7 @@ class HistoryConnector extends Component {
|
|||
}
|
||||
|
||||
HistoryConnector.propTypes = {
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchHistory: PropTypes.func.isRequired,
|
||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
||||
|
@ -155,4 +168,6 @@ HistoryConnector.propTypes = {
|
|||
clearTracks: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector);
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(HistoryConnector)
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import TrackLanguage from 'Album/TrackLanguage';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||
|
@ -131,7 +131,7 @@ class HistoryRow extends Component {
|
|||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<EpisodeLanguage
|
||||
<TrackLanguage
|
||||
language={language}
|
||||
isCutoffMet={languageCutoffNotMet}
|
||||
/>
|
||||
|
|
|
@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
|
@ -16,7 +17,9 @@ import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
|||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||
import QueueRowConnector from './QueueRowConnector';
|
||||
|
||||
class Queue extends Component {
|
||||
|
@ -42,22 +45,27 @@ class Queue extends Component {
|
|||
// before albums start fetching or when albums start fetching.
|
||||
|
||||
if (
|
||||
(
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items)
|
||||
) ||
|
||||
(!this.props.isAlbumsFetching && nextProps.isAlbumsFetching)
|
||||
hasDifferentItems(this.props.items, nextProps.items) &&
|
||||
nextProps.items.some((e) => e.albumId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.props.isAlbumsFetching && nextProps.isAlbumsFetching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState({ selectedState: {} });
|
||||
this.setState((state) => {
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -138,7 +146,7 @@ class Queue extends Component {
|
|||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isAlbumsFetching || isCheckForFinishedDownloadExecuting;
|
||||
const isAllPopulated = isPopulated && (isAlbumsPopulated || !items.length);
|
||||
const isAllPopulated = isPopulated && (isAlbumsPopulated || !items.length || items.every((e) => !e.albumId));
|
||||
const hasError = error || albumsError;
|
||||
const selectedCount = this.getSelectedIds().length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
@ -172,6 +180,21 @@ class Queue extends Component {
|
|||
onPress={this.onRemoveSelectedPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
>
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
|
@ -203,6 +226,7 @@ class Queue extends Component {
|
|||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
{...otherProps}
|
||||
optionsComponent={QueueOptionsConnector}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
>
|
||||
<TableBody>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
|
|||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as queueActions from 'Store/Actions/queueActions';
|
||||
|
@ -15,14 +16,16 @@ import Queue from './Queue';
|
|||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.albums,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
||||
(albums, queue, isCheckForFinishedDownloadExecuting) => {
|
||||
(albums, options, queue, isCheckForFinishedDownloadExecuting) => {
|
||||
return {
|
||||
isAlbumsFetching: albums.isFetching,
|
||||
isAlbumsPopulated: albums.isPopulated,
|
||||
albumsError: albums.error,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
}
|
||||
|
@ -42,19 +45,37 @@ class QueueConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
useCurrentPage,
|
||||
fetchQueue,
|
||||
gotoQueueFirstPage
|
||||
} = this.props;
|
||||
|
||||
registerPagePopulator(this.repopulate);
|
||||
this.props.gotoQueueFirstPage();
|
||||
|
||||
if (useCurrentPage) {
|
||||
fetchQueue();
|
||||
} else {
|
||||
gotoQueueFirstPage();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
const albumIds = selectUniqueIds(this.props.items, 'albumId');
|
||||
|
||||
if (albumIds.length) {
|
||||
this.props.fetchAlbums({ albumIds });
|
||||
} else {
|
||||
this.props.clearAlbums();
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.props.includeUnknownArtistItems !==
|
||||
prevProps.includeUnknownArtistItems
|
||||
) {
|
||||
this.repopulate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,4 +181,6 @@ QueueConnector.propTypes = {
|
|||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueConnector);
|
||||
export default withCurrentPage(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
class QueueOptions extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
includeUnknownArtistItems: props.includeUnknownArtistItems
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
includeUnknownArtistItems
|
||||
} = this.props;
|
||||
|
||||
if (includeUnknownArtistItems !== prevProps.includeUnknownArtistItems) {
|
||||
this.setState({
|
||||
includeUnknownArtistItems
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onOptionChange = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onOptionChange({
|
||||
[name]: value
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
includeUnknownArtistItems
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>Show Unknown Artist Items</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="includeUnknownArtistItems"
|
||||
value={includeUnknownArtistItems}
|
||||
helpText="Show items without a artist in the queue, this could include removed artists, movies or anything else in Lidarr's category"
|
||||
onChange={this.onOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
QueueOptions.propTypes = {
|
||||
includeUnknownArtistItems: PropTypes.bool.isRequired,
|
||||
onOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default QueueOptions;
|
|
@ -0,0 +1,19 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setQueueOption } from 'Store/Actions/queueActions';
|
||||
import QueueOptions from './QueueOptions';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.queue.options,
|
||||
(options) => {
|
||||
return options;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onOptionChange: setQueueOption
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueOptions);
|
|
@ -12,6 +12,7 @@ import Icon from 'Components/Icon';
|
|||
import Popover from 'Components/Tooltip/Popover';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||
import TrackLanguage from 'Album/TrackLanguage';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||
|
@ -72,6 +73,7 @@ class QueueRow extends Component {
|
|||
errorMessage,
|
||||
artist,
|
||||
album,
|
||||
language,
|
||||
quality,
|
||||
protocol,
|
||||
indexer,
|
||||
|
@ -137,21 +139,14 @@ class QueueRow extends Component {
|
|||
if (name === 'artist.sortName') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
artist ?
|
||||
<ArtistNameLink
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
/> :
|
||||
title
|
||||
}
|
||||
|
||||
if (name === 'artist') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<ArtistNameLink
|
||||
foreignArtistId={artist.foreignArtistId}
|
||||
artistName={artist.artistName}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -159,16 +154,21 @@ class QueueRow extends Component {
|
|||
if (name === 'album.title') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
album ?
|
||||
<AlbumTitleLink
|
||||
foreignAlbumId={album.foreignAlbumId}
|
||||
title={album.title}
|
||||
disambiguation={album.disambiguation}
|
||||
/>
|
||||
/> :
|
||||
'-'
|
||||
}
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'album.releaseDate') {
|
||||
if (album) {
|
||||
return (
|
||||
<RelativeDateCellConnector
|
||||
key={name}
|
||||
|
@ -177,6 +177,23 @@ class QueueRow extends Component {
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
-
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'language') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
<TrackLanguage
|
||||
language={language}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
|
@ -326,8 +343,9 @@ QueueRow.propTypes = {
|
|||
trackedDownloadStatus: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
artist: PropTypes.object.isRequired,
|
||||
album: PropTypes.object.isRequired,
|
||||
artist: PropTypes.object,
|
||||
album: PropTypes.object,
|
||||
language: PropTypes.object.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
indexer: PropTypes.string,
|
||||
|
|
|
@ -51,10 +51,6 @@ class QueueRowConnector extends Component {
|
|||
// Render
|
||||
|
||||
render() {
|
||||
if (!this.props.album) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<QueueRow
|
||||
{...this.props}
|
||||
|
|
|
@ -9,12 +9,19 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
(state) => state.app,
|
||||
(state) => state.queue.status,
|
||||
(app, status) => {
|
||||
(state) => state.queue.options.includeUnknownArtistItems,
|
||||
(app, status, includeUnknownArtistItems) => {
|
||||
const {
|
||||
count,
|
||||
unknownCount
|
||||
} = status.item;
|
||||
|
||||
return {
|
||||
isConnected: app.isConnected,
|
||||
isReconnecting: app.isReconnecting,
|
||||
isPopulated: status.isPopulated,
|
||||
...status.item
|
||||
...status.item,
|
||||
count: includeUnknownArtistItems ? count : count - unknownCount
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -100,8 +100,8 @@ class AddNewArtist extends Component {
|
|||
name="artistLookup"
|
||||
value={term}
|
||||
placeholder="eg. Breaking Benjamin, lidarr:854a1807-025b-42a8-ba8c-2a39717f1d25"
|
||||
onChange={this.onSearchInputChange}
|
||||
autoFocus={true}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
<Button
|
||||
|
|
|
@ -74,7 +74,8 @@ class AddNewArtistModalContent extends Component {
|
|||
showMetadataProfile,
|
||||
isSmallScreen,
|
||||
onModalClose,
|
||||
onInputChange
|
||||
onInputChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
|
@ -86,7 +87,8 @@ class AddNewArtistModalContent extends Component {
|
|||
<ModalBody>
|
||||
<div className={styles.container}>
|
||||
{
|
||||
!isSmallScreen &&
|
||||
isSmallScreen ?
|
||||
null:
|
||||
<div className={styles.poster}>
|
||||
<ArtistPoster
|
||||
className={styles.poster}
|
||||
|
@ -97,15 +99,19 @@ class AddNewArtistModalContent extends Component {
|
|||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
{
|
||||
overview ?
|
||||
<div className={styles.overview}>
|
||||
<TextTruncate
|
||||
truncateText="…"
|
||||
line={8}
|
||||
text={overview}
|
||||
/>
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
<Form>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Root Folder</FormLabel>
|
||||
|
||||
|
|
|
@ -107,8 +107,11 @@ class AddNewArtistSearchResult extends Component {
|
|||
{artistName}
|
||||
|
||||
{
|
||||
!name.contains(year) && !!year &&
|
||||
<span className={styles.year}>({year})</span>
|
||||
!name.contains(year) && year ?
|
||||
<span className={styles.year}>
|
||||
({year})
|
||||
</span> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -117,13 +120,14 @@ class AddNewArtistSearchResult extends Component {
|
|||
}
|
||||
|
||||
{
|
||||
isExistingArtist &&
|
||||
isExistingArtist ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={36}
|
||||
title="Already in your library"
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
@ -136,20 +140,22 @@ class AddNewArtistSearchResult extends Component {
|
|||
</Label>
|
||||
|
||||
{
|
||||
!!artistType &&
|
||||
artistType ?
|
||||
<Label size={sizes.LARGE}>
|
||||
{artistType}
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
status === 'ended' &&
|
||||
status === 'ended' ?
|
||||
<Label
|
||||
kind={kinds.DANGER}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Ended
|
||||
</Label>
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
|
|
|
@ -116,9 +116,11 @@ class ImportArtistFooter extends Component {
|
|||
isQualityProfileIdMixed,
|
||||
isLanguageProfileIdMixed,
|
||||
isMetadataProfileIdMixed,
|
||||
hasUnsearchedItems,
|
||||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
onImportPress,
|
||||
onLookupPress,
|
||||
onCancelLookupPress
|
||||
} = this.props;
|
||||
|
||||
|
@ -238,6 +240,17 @@ class ImportArtistFooter extends Component {
|
|||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
hasUnsearchedItems &&
|
||||
<Button
|
||||
className={styles.loadingButton}
|
||||
kind={kinds.SUCCESS}
|
||||
onPress={onLookupPress}
|
||||
>
|
||||
Start Processing
|
||||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
isLookingUpArtist &&
|
||||
<LoadingIndicator
|
||||
|
@ -271,10 +284,12 @@ ImportArtistFooter.propTypes = {
|
|||
isLanguageProfileIdMixed: PropTypes.bool.isRequired,
|
||||
isMetadataProfileIdMixed: PropTypes.bool.isRequired,
|
||||
isAlbumFolderMixed: PropTypes.bool.isRequired,
|
||||
hasUnsearchedItems: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired,
|
||||
onLookupPress: PropTypes.func.isRequired,
|
||||
onCancelLookupPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'lodash';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import ImportArtistFooter from './ImportArtistFooter';
|
||||
import { cancelLookupArtist } from 'Store/Actions/importArtistActions';
|
||||
import { lookupUnsearchedArtist, cancelLookupArtist } from 'Store/Actions/importArtistActions';
|
||||
|
||||
function isMixed(items, selectedIds, defaultValue, key) {
|
||||
return _.some(items, (artist) => {
|
||||
|
@ -35,6 +35,7 @@ function createMapStateToProps() {
|
|||
const isLanguageProfileIdMixed = isMixed(items, selectedIds, defaultLanguageProfileId, 'languageProfileId');
|
||||
const isMetadataProfileIdMixed = isMixed(items, selectedIds, defaultMetadataProfileId, 'metadataProfileId');
|
||||
const isAlbumFolderMixed = isMixed(items, selectedIds, defaultAlbumFolder, 'albumFolder');
|
||||
const hasUnsearchedItems = !isLookingUpArtist && items.some((item) => !item.isPopulated);
|
||||
|
||||
return {
|
||||
selectedCount: selectedIds.length,
|
||||
|
@ -49,13 +50,15 @@ function createMapStateToProps() {
|
|||
isQualityProfileIdMixed,
|
||||
isLanguageProfileIdMixed,
|
||||
isMetadataProfileIdMixed,
|
||||
isAlbumFolderMixed
|
||||
isAlbumFolderMixed,
|
||||
hasUnsearchedItems
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
onLookupPress: lookupUnsearchedArtist,
|
||||
onCancelLookupPress: cancelLookupArtist
|
||||
};
|
||||
|
||||
|
|
|
@ -110,7 +110,6 @@ ImportArtistRow.propTypes = {
|
|||
selectedArtist: PropTypes.object,
|
||||
isExistingArtist: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions';
|
||||
import { setImportArtistValue } from 'Store/Actions/importArtistActions';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import ImportArtistRow from './ImportArtistRow';
|
||||
|
||||
|
@ -34,7 +34,6 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
queueLookupArtist,
|
||||
setImportArtistValue
|
||||
};
|
||||
|
||||
|
@ -82,7 +81,6 @@ ImportArtistRowConnector.propTypes = {
|
|||
monitor: PropTypes.string,
|
||||
albumFolder: PropTypes.bool,
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
queueLookupArtist: PropTypes.func.isRequired,
|
||||
setImportArtistValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
.artistNameContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.artistName {
|
||||
margin-right: 5px;
|
||||
@add-mixin truncate;
|
||||
}
|
||||
|
||||
.disambiguation {
|
||||
|
@ -12,11 +14,6 @@
|
|||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.year {
|
||||
margin-left: 5px;
|
||||
color: $disabledColor;
|
||||
}
|
||||
|
||||
.existing {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ function ImportArtistName(props) {
|
|||
const {
|
||||
artistName,
|
||||
disambiguation,
|
||||
// year,
|
||||
isExistingArtist
|
||||
} = props;
|
||||
|
||||
|
@ -36,7 +35,6 @@ function ImportArtistName(props) {
|
|||
ImportArtistName.propTypes = {
|
||||
artistName: PropTypes.string.isRequired,
|
||||
disambiguation: PropTypes.string,
|
||||
// year: PropTypes.number.isRequired,
|
||||
isExistingArtist: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -30,8 +30,9 @@
|
|||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
flex: 1 0 auto;
|
||||
margin-left: 5px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
|
@ -68,3 +69,13 @@
|
|||
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.results {
|
||||
@add-mixin scrollbar;
|
||||
@add-mixin scrollbarTrack;
|
||||
@add-mixin scrollbarThumb;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
max-height: 165px;
|
||||
}
|
||||
|
|
|
@ -120,7 +120,7 @@ class ImportArtistSelectArtist extends Component {
|
|||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
queued,
|
||||
isQueued,
|
||||
isLookingUpArtist
|
||||
} = this.props;
|
||||
|
||||
|
@ -142,7 +142,7 @@ class ImportArtistSelectArtist extends Component {
|
|||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
isLookingUpArtist && queued && !isPopulated &&
|
||||
isLookingUpArtist && isQueued && !isPopulated &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
|
@ -170,7 +170,7 @@ class ImportArtistSelectArtist extends Component {
|
|||
|
||||
{
|
||||
isPopulated && !selectedArtist &&
|
||||
<div>
|
||||
<div className={styles.noMatches}>
|
||||
<Icon
|
||||
className={styles.warningIcon}
|
||||
name={icons.WARNING}
|
||||
|
@ -265,7 +265,7 @@ ImportArtistSelectArtist.propTypes = {
|
|||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
isQueued: PropTypes.bool.isRequired,
|
||||
isLookingUpArtist: PropTypes.bool.isRequired,
|
||||
onSearchInputChange: PropTypes.func.isRequired,
|
||||
onArtistSelect: PropTypes.func.isRequired
|
||||
|
@ -275,7 +275,7 @@ ImportArtistSelectArtist.defaultProps = {
|
|||
isFetching: true,
|
||||
isPopulated: false,
|
||||
items: [],
|
||||
queued: true
|
||||
isQueued: true
|
||||
};
|
||||
|
||||
export default ImportArtistSelectArtist;
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import ImportArtistRootFolderRow from './ImportArtistRootFolderRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
() => {
|
||||
return {
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
deleteRootFolder
|
||||
};
|
||||
|
||||
class ImportArtistRootFolderRowConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onDeletePress = () => {
|
||||
this.props.deleteRootFolder({ id: this.props.id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ImportArtistRootFolderRow
|
||||
{...this.props}
|
||||
onDeletePress={this.onDeletePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportArtistRootFolderRowConnector.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
deleteRootFolder: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistRootFolderRowConnector);
|
|
@ -8,33 +8,9 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import ImportArtistRootFolderRowConnector from './ImportArtistRootFolderRowConnector';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import styles from './ImportArtistSelectFolder.css';
|
||||
|
||||
const rootFolderColumns = [
|
||||
{
|
||||
name: 'path',
|
||||
label: 'Path',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'freeSpace',
|
||||
label: 'Free Space',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'unmappedFolders',
|
||||
label: 'Unmapped Folders',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class ImportArtistSelectFolder extends Component {
|
||||
|
||||
//
|
||||
|
@ -107,26 +83,13 @@ class ImportArtistSelectFolder extends Component {
|
|||
{
|
||||
items.length > 0 ?
|
||||
<div className={styles.recentFolders}>
|
||||
<FieldSet legend="Recent Folders">
|
||||
<Table
|
||||
columns={rootFolderColumns}
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((rootFolder) => {
|
||||
return (
|
||||
<ImportArtistRootFolderRowConnector
|
||||
key={rootFolder.id}
|
||||
id={rootFolder.id}
|
||||
path={rootFolder.path}
|
||||
freeSpace={rootFolder.freeSpace}
|
||||
unmappedFolders={rootFolder.unmappedFolders}
|
||||
<FieldSet legend="Root Folders">
|
||||
<RootFolders
|
||||
isFetching={isFetching}
|
||||
isPopulated={isPopulated}
|
||||
error={error}
|
||||
items={items}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</FieldSet>
|
||||
|
||||
<Button
|
||||
|
@ -178,8 +141,7 @@ ImportArtistSelectFolder.propTypes = {
|
|||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired,
|
||||
onDeleteRootFolderPress: PropTypes.func.isRequired
|
||||
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistSelectFolder;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
|||
import { createSelector } from 'reselect';
|
||||
import { push } from 'react-router-redux';
|
||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||
import { fetchRootFolders, addRootFolder, deleteRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import { fetchRootFolders, addRootFolder } from 'Store/Actions/rootFolderActions';
|
||||
import ImportArtistSelectFolder from './ImportArtistSelectFolder';
|
||||
|
||||
function createMapStateToProps() {
|
||||
|
@ -24,7 +24,6 @@ function createMapStateToProps() {
|
|||
const mapDispatchToProps = {
|
||||
fetchRootFolders,
|
||||
addRootFolder,
|
||||
deleteRootFolder,
|
||||
push
|
||||
};
|
||||
|
||||
|
@ -60,10 +59,6 @@ class ImportArtistSelectFolderConnector extends Component {
|
|||
this.props.addRootFolder({ path });
|
||||
}
|
||||
|
||||
onDeleteRootFolderPress = (id) => {
|
||||
this.props.deleteRootFolder({ id });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -72,7 +67,6 @@ class ImportArtistSelectFolderConnector extends Component {
|
|||
<ImportArtistSelectFolder
|
||||
{...this.props}
|
||||
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
||||
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -84,7 +78,6 @@ ImportArtistSelectFolderConnector.propTypes = {
|
|||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired,
|
||||
addRootFolder: PropTypes.func.isRequired,
|
||||
deleteRootFolder: PropTypes.func.isRequired,
|
||||
push: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { icons } from 'Helpers/Props';
|
|||
import IconButton from 'Components/Link/IconButton';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
||||
import AlbumInteractiveSearchModalConnector from './Search/AlbumInteractiveSearchModalConnector';
|
||||
import styles from './AlbumSearchCell.css';
|
||||
|
||||
class AlbumSearchCell extends Component {
|
||||
|
@ -37,6 +37,7 @@ class AlbumSearchCell extends Component {
|
|||
render() {
|
||||
const {
|
||||
albumId,
|
||||
albumTitle,
|
||||
isSearching,
|
||||
onSearchPress,
|
||||
...otherProps
|
||||
|
@ -55,9 +56,10 @@ class AlbumSearchCell extends Component {
|
|||
onPress={this.onManualSearchPress}
|
||||
/>
|
||||
|
||||
<InteractiveSearchModal
|
||||
<AlbumInteractiveSearchModalConnector
|
||||
isOpen={this.state.isDetailsModalOpen}
|
||||
albumId={albumId}
|
||||
albumTitle={albumTitle}
|
||||
onModalClose={this.onDetailsModalClose}
|
||||
{...otherProps}
|
||||
/>
|
||||
|
|
|
@ -32,13 +32,6 @@
|
|||
color: $white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
width: 250px;
|
||||
height: 97px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
flex-shrink: 0;
|
||||
margin-right: 35px;
|
||||
|
@ -61,19 +54,33 @@
|
|||
|
||||
.titleContainer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 5px;
|
||||
font-weight: 300;
|
||||
font-size: 50px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.toggleMonitoredContainer {
|
||||
align-self: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.monitorToggleButton {
|
||||
composes: toggleButton from 'Components/MonitorToggleButton.css';
|
||||
|
||||
width: 40px;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.alternateTitlesIconContainer {
|
||||
align-self: flex-end;
|
||||
margin-left: 20px;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.albumNavigationButtons {
|
||||
|
@ -87,14 +94,19 @@
|
|||
width: 30px;
|
||||
color: #e1e2e3;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: $iconButtonHoverLightColor;
|
||||
}
|
||||
}
|
||||
|
||||
.details {
|
||||
margin-bottom: 8px;
|
||||
font-weight: 300;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.runtime {
|
||||
.duration {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
|
@ -115,7 +127,9 @@
|
|||
|
||||
.overview {
|
||||
flex: 1 0 auto;
|
||||
margin-top: 8px;
|
||||
min-height: 0;
|
||||
font-size: $intermediateFontSize;
|
||||
}
|
||||
|
||||
.contentContainer {
|
||||
|
|
|
@ -12,6 +12,7 @@ import HeartRating from 'Components/HeartRating';
|
|||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Label from 'Components/Label';
|
||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import AlbumCover from 'Album/AlbumCover';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
|
@ -25,7 +26,7 @@ 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 InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
||||
import AlbumInteractiveSearchModalConnector from 'Album/Search/AlbumInteractiveSearchModalConnector';
|
||||
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
||||
import AlbumDetailsLinks from './AlbumDetailsLinks';
|
||||
import styles from './AlbumDetails.css';
|
||||
|
@ -41,6 +42,28 @@ function getFanartUrl(images) {
|
|||
}
|
||||
}
|
||||
|
||||
function formatDuration(timeSpan) {
|
||||
const duration = moment.duration(timeSpan);
|
||||
const hours = duration.get('hours');
|
||||
const minutes = duration.get('minutes');
|
||||
let hoursText = 'Hours';
|
||||
let minText = 'Minutes';
|
||||
|
||||
if (minutes === 1) {
|
||||
minText = 'Minute';
|
||||
}
|
||||
|
||||
if (hours === 0) {
|
||||
return `${minutes} ${minText}`;
|
||||
}
|
||||
|
||||
if (hours === 1) {
|
||||
hoursText = 'Hour';
|
||||
}
|
||||
|
||||
return `${hours} ${hoursText} ${minutes} ${minText}`;
|
||||
}
|
||||
|
||||
function getExpandedState(newState) {
|
||||
return {
|
||||
allExpanded: newState.allSelected,
|
||||
|
@ -144,6 +167,7 @@ class AlbumDetails extends Component {
|
|||
foreignAlbumId,
|
||||
title,
|
||||
disambiguation,
|
||||
duration,
|
||||
overview,
|
||||
albumType,
|
||||
statistics = {},
|
||||
|
@ -153,6 +177,7 @@ class AlbumDetails extends Component {
|
|||
images,
|
||||
links,
|
||||
media,
|
||||
isSaving,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
albumsError,
|
||||
|
@ -162,6 +187,7 @@ class AlbumDetails extends Component {
|
|||
previousAlbum,
|
||||
nextAlbum,
|
||||
isSearching,
|
||||
onMonitorTogglePress,
|
||||
onSearchPress
|
||||
} = this.props;
|
||||
|
||||
|
@ -259,10 +285,23 @@ class AlbumDetails extends Component {
|
|||
/>
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.titleRow}>
|
||||
<div className={styles.titleContainer}>
|
||||
|
||||
<div className={styles.toggleMonitoredContainer}>
|
||||
<MonitorToggleButton
|
||||
className={styles.monitorToggleButton}
|
||||
monitored={monitored}
|
||||
isSaving={isSaving}
|
||||
size={40}
|
||||
onPress={onMonitorTogglePress}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>
|
||||
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.albumNavigationButtons}>
|
||||
<IconButton
|
||||
|
@ -293,6 +332,13 @@ class AlbumDetails extends Component {
|
|||
|
||||
<div className={styles.details}>
|
||||
<div>
|
||||
{
|
||||
!!duration &&
|
||||
<span className={styles.duration}>
|
||||
{formatDuration(duration)}
|
||||
</span>
|
||||
}
|
||||
|
||||
<HeartRating
|
||||
rating={ratings.value}
|
||||
iconSize={20}
|
||||
|
@ -456,9 +502,10 @@ class AlbumDetails extends Component {
|
|||
onModalClose={this.onManageTracksModalClose}
|
||||
/>
|
||||
|
||||
<InteractiveSearchModal
|
||||
<AlbumInteractiveSearchModalConnector
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
albumId={id}
|
||||
albumTitle={title}
|
||||
onModalClose={this.onInteractiveSearchModalClose}
|
||||
/>
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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 (
|
||||
<AlbumDetails
|
||||
{...this.props}
|
||||
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||
onSearchPress={this.onSearchPress}
|
||||
/>
|
||||
);
|
||||
|
@ -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
|
||||
};
|
||||
|
||||
|
|
|
@ -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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AlbumInteractiveSearchModalContent
|
||||
albumId={albumId}
|
||||
albumTitle={albumTitle}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AlbumInteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
albumId: PropTypes.number.isRequired,
|
||||
albumTitle: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AlbumInteractiveSearchModal;
|
|
@ -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);
|
|
@ -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 (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Interactive Search {albumId != null && `- ${albumTitle}`}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<InteractiveSearchConnector
|
||||
type="album"
|
||||
searchPayload={{
|
||||
albumId
|
||||
}}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AlbumInteractiveSearchModalContent.propTypes = {
|
||||
albumId: PropTypes.number.isRequired,
|
||||
albumTitle: PropTypes.string.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AlbumInteractiveSearchModalContent;
|
|
@ -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;
|
|
@ -12,6 +12,7 @@ function AppUpdatedModal(props) {
|
|||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AppUpdatedModalContentConnector
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
const ColorImpairedContext = React.createContext(false);
|
||||
export const ColorImpairedConsumer = ColorImpairedContext.Consumer;
|
||||
|
||||
export default ColorImpairedContext;
|
|
@ -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 bannerPlaceholder = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA+gAAAC5AgMAAADG9/24AAAADFBMVEUyMjI7Ozs1NTU4ODjgOsZvAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QkRBgAc5PUQ8QAAB7tJREFUeNrtnb1rHEsMwMdjArtrUrpf3uMg2cOl+5QpXWRvjQkXlyHVK8MVYXFl3F+/GAzrNfdSuXkQnH9ie/Oqw32aFM6bkTQfd85rHrwi0qjJJql+pxlJo9FISv0XqX8mKkmSJEmSJEmSJEmSJEmSJEmS5NeVYrh7HDqJ5DeY23kQB64/u7zW91amzgXqvRqjfGYvarnfxqncuaQlf72Zxv4gSOluudOfjRy1Hzh1L+nPj+KU3jh0MWr3Sp87dDFq98An/msmg3zPW/aFR6/vRaBPglML6Mci0H0g92MYfrjvRgJ5TrDvhuFyGIZv9NdTOev93dDry1Z59mM56/2hU8WZcefFjZgVT/Z90SlEV9VKio3HeKYZLLSGII6WPP+oBv3ZwkL3iE4JG/ZRjUYb16mAripUO/c4Hl3bd/juCF19FuHeJn6nK90VBh0s3SjBvcFW/wTrvfBa118EbHY8qmMOtmgdupoKOLTvAmOH1k059LKAX+Qra/TncExH4hcBXV3Zf/+Dv5Vb43cfod/wt3PLsN5VF6HDiudt56IbB23Ql5/oZ8A7CfZWbgF61sap62XdlOZTZ2rF3c7ltNW1+f7LuDez/uc6uDfO8dwkbPXcKF8vPS9sds527pC2utH0uCb0Jmz2t8wNvN3q2jj4ntDJna94m3g4sb7HH8Gue0RH4Je8z61g4GGr79Wz1qHjbi94m/jcH1IOccsj+lt/sOFr4m0EPz+3XyNueURvSmfn+Ebx1rctMvOxU8foqOwVa+9mfdvHDH+DdYQOxAesvZslvc/wo4vQZy6eY+vdrCVrut/AyTW9CWvO3E1ra4L6i5Fxoka7MDan45vTOmx2MPGc0QH52Tb6kTPxXNGtW5/Tnl+oGP2N/dstY8eeO2M+bqM3zvVxRT+gS8Vdl5/z6CaCzfx/c41o3pP2+030UzrAHDNGv8d4FvMVAf1I4c07V/QlnNsyG9TN23ID/ZjObnO+6CZmydS+y8oG9Dfk2Gd80WcW3Rv44e5bZOLtD8EUHbRq0V0F/DAMn8fQ2UUv2UayEMxplaFvM0G7QR/ufqBcmH/gG85Z9BM8rMO5bdiUDu4ceaLvUjqWfJveQm8hpvnKFv1jOLxUTtkoBWf0nIK5Cd6wO207dAznTlmjH+K630KvuKPbOHYffJsOexykx0iWJ7pNRf+ttEXvW1U49F7ZbJ26RHSe6ehn5NRGMPBVQAfP12EQf8QZPTMxXac30M1RxtaWXLBFn3j0eRsHNIBuCycLtqfWCQZrBcZ0W+gVhXts0RtEXyit4zDOoE/N/5yNzNF3oVB0A93I6ise7bijr1XwbYiurySg7xjfpp+g375mjj5D9HYD3fr6YvkKcxU80Q8duvVt2+gjob/ljX7yFH1a80dvDfpia8GXqp3Wr1XJXevljvFt2QZ6a6tJ2Gvd2Pa8Xuu2iNB7o++r+lUJlQas93oOl04be73Ut7y1Ts4tn/0EfaxZOzcKaXIsqNiI4QtE5x7N7Z08RZ9CKpb38cWs9V26b4sDWURnr3W9foq+gpM8a3S4V+rhKUCcqrBXTufMUxU2QWUTkFtZGls3pjgnqJ4FdB1lZAd8/qMEZGRtAlJvJqONb2syzuj2CuK+1YU5reg2CzGNyqq6fpOpku8VRI7lchX+7SIy8NdQSaKn3K8bsWJOlZGBX2Ed0bUIdBXftJpzG1h2vjetWFqgHXrhczTWtx2h8llXVTytI4FHnaes0bGMqNhC1+jUXmFMx7iCaq6qy4HqxR7dFfMUtc34LQCVDPppcM13Kie5RmTGJYNUKBraDL57xEKalS0UzTgXiiqn1X3fRxR0bBf6TLEvD4aKkslTdO5F4fQUIHseo5ugfrShewbWjvsriHxT65WAByAHVCT7u0fvoKDC+rZClZyf/eSAngUTj1pfCXjshU/8smDiPTr7J374sDPDfI1Hd4cX1g874TmvRc+30U8V9+e88Ig7U6V26Ofk21rzg1ScH3Hj033bTXIZab2iGG6PdWOaQzLx7cShQ0FFfdxqlfFu2DAhxw4vf5zWV3hYZ96m47l7u0+DMAgdu1Dxbs4St+Rx6MbAwzIveLfk8Y2Y9J5HP1tij3DmjZii9lujQy9GXO/M22/5pmvUVdWiV3RkYd50zbfaI7Vb9GmDD0C4t9qLGiy+JPTVKT4A4d5gMY866N4i+p9KuUM767aavpkqLnmDDl10S8W/mSq20O3I3n8x6AXufP4tdKlxcpkZ4HN43FZ0mT3GCmicTO2ysW2uVXmFWp/yb5cdN0kHrb/ADwFN0uPW+ICOt+0SWuPjkW2tPTr+CjcSphjiGIzWodPQGwljMGj4Se/Q0bfJGH4Sj7zx6DJG3tCgo4HQoYiukDHoKB5vZdCtb9MrIUM7DyK169Zu+mEUMtQsjLJrtT7vWl2IGWXnBxjaFwFnraQBhmFs5dDqi66SNLYyGlY6XMgaVip4RG00mHh2LWwwcRhHPVsJG0cdhpDProQNIQ+j52e30kbPa19B5T64H9Wfqn0mTelB7Y04pWMFfCQf5JDTjYOTuSClu5QUSa9EyU0gf5BF7iaP2zxdq6TJjUydgxTD3aPvm5wkSZIkSZIkSZIkSZIkSZIk+Z9kv/534U2+Uyf0XwP9H83PZBlAqkdjAAAAAElFTkSuQmCC';
|
||||
|
||||
function findBanner(images) {
|
||||
return _.find(images, { coverType: 'banner' });
|
||||
}
|
||||
|
||||
function getBannerUrl(banner, size) {
|
||||
if (banner) {
|
||||
if (banner.url.contains('lastWrite=') || (/^https?:/).test(banner.url)) {
|
||||
// Remove protocol
|
||||
let url = banner.url.replace(/^https?:/, '');
|
||||
url = url.replace('banner.jpg', `banner-${size}.jpg`);
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistBanner extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const pixelRatio = Math.floor(window.devicePixelRatio);
|
||||
|
||||
const {
|
||||
images,
|
||||
size
|
||||
} = props;
|
||||
|
||||
const banner = findBanner(images);
|
||||
|
||||
this.state = {
|
||||
pixelRatio,
|
||||
banner,
|
||||
bannerUrl: getBannerUrl(banner, pixelRatio * size),
|
||||
isLoaded: false,
|
||||
hasError: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
images,
|
||||
size
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
banner,
|
||||
pixelRatio
|
||||
} = this.state;
|
||||
|
||||
const nextBanner = findBanner(images);
|
||||
|
||||
if (nextBanner && (!banner || nextBanner.url !== banner.url)) {
|
||||
this.setState({
|
||||
banner: nextBanner,
|
||||
bannerUrl: getBannerUrl(nextBanner, pixelRatio * size),
|
||||
hasError: false,
|
||||
isLoaded: true
|
||||
});
|
||||
} else if (!nextBanner && banner) {
|
||||
this.setState({
|
||||
banner: nextBanner,
|
||||
bannerUrl: bannerPlaceholder,
|
||||
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 {
|
||||
bannerUrl,
|
||||
hasError,
|
||||
isLoaded
|
||||
} = this.state;
|
||||
|
||||
if (hasError || !bannerUrl) {
|
||||
function ArtistBanner(props) {
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={bannerPlaceholder}
|
||||
<ArtistImage
|
||||
{...props}
|
||||
coverType="banner"
|
||||
placeholder={bannerPlaceholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (lazy) {
|
||||
return (
|
||||
<LazyLoad
|
||||
height={size}
|
||||
offset={100}
|
||||
overflow={overflow}
|
||||
placeholder={
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={bannerPlaceholder}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={bannerUrl}
|
||||
onError={this.onError}
|
||||
/>
|
||||
</LazyLoad>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={isLoaded ? bannerUrl : bannerPlaceholder}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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 (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={placeholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (lazy) {
|
||||
return (
|
||||
<LazyLoad
|
||||
height={size}
|
||||
offset={100}
|
||||
overflow={overflow}
|
||||
placeholder={
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={placeholder}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={url}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
</LazyLoad>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={isLoaded ? url : placeholder}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -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) {
|
||||
function ArtistPoster(props) {
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={posterPlaceholder}
|
||||
<ArtistImage
|
||||
{...props}
|
||||
coverType="poster"
|
||||
placeholder={posterPlaceholder}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (lazy) {
|
||||
return (
|
||||
<LazyLoad
|
||||
height={size}
|
||||
offset={100}
|
||||
overflow={overflow}
|
||||
placeholder={
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={posterPlaceholder}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={posterUrl}
|
||||
onError={this.onError}
|
||||
/>
|
||||
</LazyLoad>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className={className}
|
||||
style={style}
|
||||
src={isLoaded ? posterUrl : posterPlaceholder}
|
||||
onError={this.onError}
|
||||
onLoad={this.onLoad}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
|||
<PageToolbarButton
|
||||
label="Search Monitored"
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!monitored || !hasMonitoredAlbums}
|
||||
isDisabled={!monitored || !hasMonitoredAlbums || !hasAlbums}
|
||||
isSpinning={isSearching}
|
||||
title={hasMonitoredAlbums ? undefined : 'No monitored albums for this artist'}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Interactive Search"
|
||||
iconName={icons.INTERACTIVE}
|
||||
isDisabled={!monitored || !hasMonitoredAlbums || !hasAlbums}
|
||||
isSpinning={isSearching}
|
||||
title={hasMonitoredAlbums ? undefined : 'No monitored albums for this artist'}
|
||||
onPress={this.onInteractiveSearchPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="Preview Rename"
|
||||
iconName={icons.ORGANIZE}
|
||||
isDisabled={!hasTrackFiles}
|
||||
onPress={this.onOrganizePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manage Tracks"
|
||||
iconName={icons.TRACK_FILE}
|
||||
isDisabled={!hasTrackFiles}
|
||||
onPress={this.onManageTracksPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="History"
|
||||
iconName={icons.HISTORY}
|
||||
isDisabled={!hasAlbums}
|
||||
onPress={this.onArtistHistoryPress}
|
||||
/>
|
||||
|
||||
|
@ -609,6 +633,12 @@ class ArtistDetails extends Component {
|
|||
showImportMode={false}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
|
||||
<ArtistInteractiveSearchModalConnector
|
||||
isOpen={isInteractiveSearchModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onInteractiveSearchModalClose}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
composes: menuContent from 'Components/Menu/MenuContent.css';
|
||||
|
||||
white-space: nowrap;
|
||||
font-size: 14px;
|
||||
font-size: $defaultFontSize;
|
||||
}
|
||||
|
||||
.actionMenuIcon {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -85,9 +85,7 @@ class EditArtistModalContent extends Component {
|
|||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Form
|
||||
{...otherProps}
|
||||
>
|
||||
<Form {...otherProps}>
|
||||
<FormGroup>
|
||||
<FormLabel>Monitored</FormLabel>
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import TrackLanguage from 'Album/TrackLanguage';
|
||||
import TrackQuality from 'Album/TrackQuality';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
|
@ -68,8 +68,6 @@ class ArtistHistoryRow extends Component {
|
|||
qualityCutoffNotMet,
|
||||
date,
|
||||
data,
|
||||
fullArtist,
|
||||
artist,
|
||||
album
|
||||
} = this.props;
|
||||
|
||||
|
@ -93,7 +91,7 @@ class ArtistHistoryRow extends Component {
|
|||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeLanguage
|
||||
<TrackLanguage
|
||||
language={language}
|
||||
isCutoffNotMet={languageCutoffNotMet}
|
||||
/>
|
||||
|
|
|
@ -7,12 +7,14 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
|||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector';
|
||||
import ArtistIndexTableOptionsConnector from './Table/ArtistIndexTableOptionsConnector';
|
||||
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
|
||||
import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector';
|
||||
import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal';
|
||||
|
@ -187,6 +189,7 @@ class ArtistIndex extends Component {
|
|||
error,
|
||||
totalItems,
|
||||
items,
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
|
@ -245,35 +248,52 @@ class ArtistIndex extends Component {
|
|||
alignContent={align.RIGHT}
|
||||
collapseButtons={false}
|
||||
>
|
||||
{
|
||||
view === 'table' ?
|
||||
<TableOptionsModalWrapper
|
||||
{...otherProps}
|
||||
columns={columns}
|
||||
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||
>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.TABLE}
|
||||
/>
|
||||
</TableOptionsModalWrapper> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'posters' &&
|
||||
view === 'posters' ?
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.POSTER}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={this.onPosterOptionsPress}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'banners' &&
|
||||
view === 'banners' ?
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.POSTER}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={this.onBannerOptionsPress}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
view === 'overview' &&
|
||||
view === 'overview' ?
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.OVERVIEW}
|
||||
isDisabled={hasNoArtist}
|
||||
onPress={this.onOverviewOptionsPress}
|
||||
/>
|
||||
/> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -382,6 +402,7 @@ ArtistIndex.propTypes = {
|
|||
error: PropTypes.object,
|
||||
totalItems: PropTypes.number.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
|
|
|
@ -9,7 +9,7 @@ import createCommandExecutingSelector from 'Store/Selectors/createCommandExecuti
|
|||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||
import scrollPositions from 'Store/scrollPositions';
|
||||
import { setArtistSort, setArtistFilter, setArtistView } from 'Store/Actions/artistIndexActions';
|
||||
import { setArtistSort, setArtistFilter, setArtistView, setArtistTableOption } from 'Store/Actions/artistIndexActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withScrollPosition from 'Components/withScrollPosition';
|
||||
|
@ -66,13 +66,41 @@ function createMapStateToProps() {
|
|||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchArtist,
|
||||
setArtistSort,
|
||||
setArtistFilter,
|
||||
setArtistView,
|
||||
executeCommand
|
||||
};
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
dispatchFetchArtist() {
|
||||
dispatch(fetchArtist);
|
||||
},
|
||||
|
||||
onTableOptionChange(payload) {
|
||||
dispatch(setArtistTableOption(payload));
|
||||
},
|
||||
|
||||
onSortSelect(sortKey) {
|
||||
dispatch(setArtistSort({ sortKey }));
|
||||
},
|
||||
|
||||
onFilterSelect(selectedFilterKey) {
|
||||
dispatch(setArtistFilter({ selectedFilterKey }));
|
||||
},
|
||||
|
||||
dispatchSetArtistView(view) {
|
||||
dispatch(setArtistView({ view }));
|
||||
},
|
||||
|
||||
onRefreshArtistPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.REFRESH_ARTIST
|
||||
}));
|
||||
},
|
||||
|
||||
onRssSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RSS_SYNC
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class ArtistIndexConnector extends Component {
|
||||
|
||||
|
@ -94,24 +122,16 @@ class ArtistIndexConnector extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchArtist();
|
||||
this.props.dispatchFetchArtist();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortSelect = (sortKey) => {
|
||||
this.props.setArtistSort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (selectedFilterKey) => {
|
||||
this.props.setArtistFilter({ selectedFilterKey });
|
||||
}
|
||||
|
||||
onViewSelect = (view) => {
|
||||
// Reset the scroll position before changing the view
|
||||
this.setState({ scrollTop: 0 }, () => {
|
||||
this.props.setArtistView({ view });
|
||||
this.props.dispatchSetArtistView(view);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -123,18 +143,6 @@ class ArtistIndexConnector extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onRefreshArtistPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_ARTIST
|
||||
});
|
||||
}
|
||||
|
||||
onRssSyncPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RSS_SYNC
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -143,12 +151,8 @@ class ArtistIndexConnector extends Component {
|
|||
<ArtistIndex
|
||||
{...this.props}
|
||||
scrollTop={this.state.scrollTop}
|
||||
onSortSelect={this.onSortSelect}
|
||||
onFilterSelect={this.onFilterSelect}
|
||||
onViewSelect={this.onViewSelect}
|
||||
onScroll={this.onScroll}
|
||||
onRefreshArtistPress={this.onRefreshArtistPress}
|
||||
onRssSyncPress={this.onRssSyncPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -158,14 +162,10 @@ ArtistIndexConnector.propTypes = {
|
|||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
view: PropTypes.string.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
fetchArtist: PropTypes.func.isRequired,
|
||||
setArtistSort: PropTypes.func.isRequired,
|
||||
setArtistFilter: PropTypes.func.isRequired,
|
||||
setArtistView: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
dispatchFetchArtist: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default withScrollPosition(
|
||||
connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexConnector),
|
||||
connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexConnector),
|
||||
'artistIndex'
|
||||
);
|
||||
|
|
|
@ -34,12 +34,20 @@
|
|||
composes: legendItemColor;
|
||||
|
||||
background-color: $dangerColor;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(90deg, color($dangerColor shade(5%)), color($dangerColor shade(5%)) 5px, color($dangerColor shade(15%)) 5px, color($dangerColor shade(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.missingUnmonitored {
|
||||
composes: legendItemColor;
|
||||
|
||||
background-color: $warningColor;
|
||||
|
||||
&:global(.colorImpaired) {
|
||||
background: repeating-linear-gradient(45deg, $warningColor, $warningColor 5px, color($warningColor tint(15%)) 5px, color($warningColor tint(15%)) 10px);
|
||||
}
|
||||
}
|
||||
|
||||
.statistics {
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import styles from './ArtistIndexFooter.css';
|
||||
|
@ -39,26 +41,49 @@ function ArtistIndexFooter({ artist }) {
|
|||
totalFileSize += sizeOnDisk;
|
||||
});
|
||||
|
||||
return (
|
||||
<ColorImpairedConsumer>
|
||||
{(enableColorImpairedMode) => {
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
<div>
|
||||
<div className={styles.legendItem}>
|
||||
<div className={styles.continuing} />
|
||||
<div
|
||||
className={classNames(
|
||||
styles.continuing,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Continuing (All tracks downloaded)</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div className={styles.ended} />
|
||||
<div
|
||||
className={classNames(
|
||||
styles.ended,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Ended (All tracks downloaded)</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div className={styles.missingMonitored} />
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingMonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Missing Tracks (Artist monitored)</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.legendItem}>
|
||||
<div className={styles.missingUnmonitored} />
|
||||
<div
|
||||
className={classNames(
|
||||
styles.missingUnmonitored,
|
||||
enableColorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
<div>Missing Tracks (Artist not monitored)</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -114,6 +139,9 @@ function ArtistIndexFooter({ artist }) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</ColorImpairedConsumer>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistIndexFooter.propTypes = {
|
||||
|
|
|
@ -26,10 +26,27 @@ $hoverScale: 1.05;
|
|||
.link {
|
||||
composes: link from 'Components/Link/Link.css';
|
||||
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 70px;
|
||||
background-color: $defaultColor;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.nextAiring {
|
||||
background-color: #fafbfc;
|
||||
text-align: center;
|
||||
|
@ -49,6 +66,7 @@ $hoverScale: 1.05;
|
|||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-width: 0 25px 25px 0;
|
||||
|
|
|
@ -22,6 +22,7 @@ class ArtistIndexPoster extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
hasPosterError: false,
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false
|
||||
};
|
||||
|
@ -49,6 +50,18 @@ class ArtistIndexPoster extends Component {
|
|||
this.setState({ isDeleteArtistModalOpen: false });
|
||||
}
|
||||
|
||||
onPosterLoad = () => {
|
||||
if (this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: false });
|
||||
}
|
||||
}
|
||||
|
||||
onPosterLoadError = () => {
|
||||
if (!this.state.hasPosterError) {
|
||||
this.setState({ hasPosterError: true });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -90,6 +103,7 @@ class ArtistIndexPoster extends Component {
|
|||
} = statistics;
|
||||
|
||||
const {
|
||||
hasPosterError,
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen
|
||||
} = this.state;
|
||||
|
@ -153,7 +167,17 @@ class ArtistIndexPoster extends Component {
|
|||
size={250}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={this.onPosterLoadError}
|
||||
onLoad={this.onPosterLoad}
|
||||
/>
|
||||
|
||||
{
|
||||
hasPosterError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{artistName}
|
||||
</div>
|
||||
}
|
||||
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -1,47 +1,22 @@
|
|||
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() {
|
||||
function ArtistIndexHeader(props) {
|
||||
const {
|
||||
showSearchAction,
|
||||
showBanners,
|
||||
columns,
|
||||
onTableOptionChange,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<VirtualTableHeader>
|
||||
|
@ -67,10 +42,16 @@ class ArtistIndexHeader extends Component {
|
|||
isSortable={false}
|
||||
{...otherProps}
|
||||
>
|
||||
|
||||
<TableOptionsModalWrapper
|
||||
columns={columns}
|
||||
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
>
|
||||
<IconButton
|
||||
name={icons.ADVANCED_SETTINGS}
|
||||
onPress={this.onTableOptionsPress}
|
||||
/>
|
||||
</TableOptionsModalWrapper>
|
||||
</VirtualTableHeaderCell>
|
||||
);
|
||||
}
|
||||
|
@ -78,7 +59,11 @@ class ArtistIndexHeader extends Component {
|
|||
return (
|
||||
<VirtualTableHeaderCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
className={classNames(
|
||||
styles[name],
|
||||
name === 'sortName' && showBanners && styles.banner,
|
||||
name === 'sortName' && showBanners && !hasGrowableColumns(columns) && styles.bannerGrow
|
||||
)}
|
||||
name={name}
|
||||
isSortable={isSortable}
|
||||
{...otherProps}
|
||||
|
@ -88,22 +73,14 @@ class ArtistIndexHeader extends Component {
|
|||
);
|
||||
})
|
||||
}
|
||||
|
||||
<TableOptionsModal
|
||||
isOpen={this.state.isTableOptionsModalOpen}
|
||||
columns={columns}
|
||||
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||
onTableOptionChange={onTableOptionChange}
|
||||
onModalClose={this.onTableOptionsModalClose}
|
||||
/>
|
||||
</VirtualTableHeader>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexHeader.propTypes = {
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
onTableOptionChange: PropTypes.func.isRequired,
|
||||
showBanners: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default ArtistIndexHeader;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 (
|
||||
<VirtualTableRowCell
|
||||
key={name}
|
||||
className={styles[name]}
|
||||
className={classNames(
|
||||
styles[name],
|
||||
showBanners && styles.banner,
|
||||
showBanners && !hasGrowableColumns(columns) && styles.bannerGrow
|
||||
)}
|
||||
>
|
||||
{
|
||||
showBanners ?
|
||||
<Link
|
||||
className={styles.link}
|
||||
to={`/artist/${foreignArtistId}`}
|
||||
>
|
||||
<ArtistBanner
|
||||
className={styles.bannerImage}
|
||||
images={images}
|
||||
lazy={false}
|
||||
overflow={true}
|
||||
onError={this.onBannerLoadError}
|
||||
onLoad={this.onBannerLoad}
|
||||
/>
|
||||
|
||||
{
|
||||
hasBannerError &&
|
||||
<div className={styles.overlayTitle}>
|
||||
{artistName}
|
||||
</div>
|
||||
}
|
||||
</Link> :
|
||||
|
||||
<ArtistNameLink
|
||||
foreignArtistId={foreignArtistId}
|
||||
artistName={artistName}
|
||||
/>
|
||||
}
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
@ -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,
|
||||
|
|
|
@ -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={
|
||||
<ArtistIndexHeaderConnector
|
||||
showBanners={showBanners}
|
||||
columns={columns}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
|
@ -116,6 +120,7 @@ ArtistIndexTable.propTypes = {
|
|||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
sortKey: PropTypes.string,
|
||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||
showBanners: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
jumpToCharacter: PropTypes.string,
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
|
|
|
@ -11,7 +11,8 @@ function createMapStateToProps() {
|
|||
(dimensions, artist) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen,
|
||||
...artist
|
||||
...artist,
|
||||
showBanners: artist.tableOptions.showBanners
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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,10 +57,24 @@ class ArtistIndexTableOptions extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
showBanners,
|
||||
showSearchAction
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<FormGroup>
|
||||
<FormLabel>Show Banners</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showBanners"
|
||||
value={showBanners}
|
||||
helpText="Show banners instead of names"
|
||||
onChange={this.onTableOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Show Search</FormLabel>
|
||||
|
||||
|
@ -60,15 +82,17 @@ class ArtistIndexTableOptions extends Component {
|
|||
type={inputTypes.CHECK}
|
||||
name="showSearchAction"
|
||||
value={showSearchAction}
|
||||
helpText="Show search button"
|
||||
helpText="Show search button on hover"
|
||||
onChange={this.onTableOptionChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistIndexTableOptions.propTypes = {
|
||||
showBanners: PropTypes.bool.isRequired,
|
||||
showSearchAction: PropTypes.bool.isRequired,
|
||||
onTableOptionChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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 (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ArtistInteractiveSearchModalContent
|
||||
artistId={artistId}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistInteractiveSearchModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
artistId: PropTypes.number.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistInteractiveSearchModal;
|
|
@ -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);
|
|
@ -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 (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Interactive Search
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<InteractiveSearchConnector
|
||||
type="artist"
|
||||
searchPayload={{
|
||||
artistId
|
||||
}}
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistInteractiveSearchModalContent.propTypes = {
|
||||
artistId: PropTypes.number.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistInteractiveSearchModalContent;
|
|
@ -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;
|
||||
}
|
||||
|
||||
.status {
|
||||
position: absolute;
|
||||
top: 7%;
|
||||
left: 0;
|
||||
height: 86%;
|
||||
.eventWrapper {
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.date,
|
||||
|
|
|
@ -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 {
|
|||
|
||||
<div
|
||||
className={classNames(
|
||||
styles.status,
|
||||
styles[statusStyle]
|
||||
styles.eventWrapper,
|
||||
styles[statusStyle],
|
||||
colorImpairedMode && 'colorImpaired'
|
||||
)}
|
||||
/>
|
||||
|
||||
|
|
|
@ -15,7 +15,8 @@ function createMapStateToProps() {
|
|||
artist,
|
||||
queueItem,
|
||||
timeFormat: uiSettings.timeFormat,
|
||||
longDateFormat: uiSettings.longDateFormat
|
||||
longDateFormat: uiSettings.longDateFormat,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||
};
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
isCalendarLinkModalOpen,
|
||||
isOptionsModalOpen
|
||||
} = this.state;
|
||||
|
||||
const isMeasured = this.state.width > 0;
|
||||
|
||||
let PageComponent = 'div';
|
||||
|
||||
if (isMeasured) {
|
||||
PageComponent = hasArtist ? CalendarConnector : NoArtist;
|
||||
}
|
||||
const PageComponent = hasArtist ? CalendarConnector : NoArtist;
|
||||
|
||||
return (
|
||||
<PageContent title="Calendar">
|
||||
|
@ -77,9 +99,23 @@ class CalendarPage extends Component {
|
|||
iconName={icons.CALENDAR}
|
||||
onPress={this.onGetCalendarLinkPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Search for Missing"
|
||||
iconName={icons.SEARCH}
|
||||
isDisabled={!missingAlbumIds.length}
|
||||
isSpinning={isSearchingForMissing}
|
||||
onPress={this.onSearchMissingPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<PageToolbarSection alignContent={align.RIGHT}>
|
||||
<PageToolbarButton
|
||||
label="Options"
|
||||
iconName={icons.POSTER}
|
||||
onPress={this.onOptionsPress}
|
||||
/>
|
||||
|
||||
<FilterMenu
|
||||
alignMenu={align.RIGHT}
|
||||
isDisabled={!hasArtist}
|
||||
|
@ -99,19 +135,31 @@ class CalendarPage extends Component {
|
|||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<PageComponent />
|
||||
{
|
||||
isMeasured ?
|
||||
<PageComponent
|
||||
useCurrentPage={useCurrentPage}
|
||||
/> :
|
||||
<div />
|
||||
}
|
||||
</Measure>
|
||||
|
||||
{
|
||||
hasArtist &&
|
||||
<Legend colorImpairedMode={colorImpairedMode} />
|
||||
<LegendConnector />
|
||||
}
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<CalendarLinkModal
|
||||
isOpen={this.state.isCalendarLinkModalOpen}
|
||||
isOpen={isCalendarLinkModalOpen}
|
||||
onModalClose={this.onGetCalendarLinkModalClose}
|
||||
/>
|
||||
|
||||
<CalendarOptionsModal
|
||||
isOpen={isOptionsModalOpen}
|
||||
onModalClose={this.onOptionsModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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)
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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(
|
||||
<LegendIconItem
|
||||
name="Cutoff Not Met"
|
||||
icon={icons.TRACK_FILE}
|
||||
kind={kinds.WARNING}
|
||||
tooltip="Quality or language cutoff has not been met"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.legend}>
|
||||
<div>
|
||||
|
@ -47,11 +67,24 @@ function Legend({ colorImpairedMode }) {
|
|||
colorImpairedMode={colorImpairedMode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{iconsToShow[0]}
|
||||
</div>
|
||||
|
||||
{
|
||||
iconsToShow.length > 1 &&
|
||||
<div>
|
||||
{iconsToShow[1]}
|
||||
{iconsToShow[2]}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Legend.propTypes = {
|
||||
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||
colorImpairedMode: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
|
@ -0,0 +1,10 @@
|
|||
.legendIconItem {
|
||||
margin: 3px 0;
|
||||
margin-right: 6px;
|
||||
width: 150px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 5px;
|
||||
}
|
|
@ -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 (
|
||||
<div
|
||||
className={styles.legendIconItem}
|
||||
title={tooltip}
|
||||
>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
name={icon}
|
||||
kind={kind}
|
||||
/>
|
||||
|
||||
{name}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
LegendIconItem.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
icon: PropTypes.object.isRequired,
|
||||
kind: PropTypes.string.isRequired,
|
||||
tooltip: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default LegendIconItem;
|
|
@ -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}
|
||||
>
|
||||
<InteractiveSearchModalContentConnector
|
||||
{...otherProps}
|
||||
<CalendarOptionsModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
InteractiveSearchModal.propTypes = {
|
||||
CalendarOptionsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default InteractiveSearchModal;
|
||||
export default CalendarOptionsModal;
|
|
@ -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 (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Calendar Options
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<FieldSet legend="Local">
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>Collapse Multiple Albums</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="collapseMultipleAlbums"
|
||||
value={collapseMultipleAlbums}
|
||||
helpText="Collapse multiple albums releasing on the same day"
|
||||
onChange={this.onOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Icon for Cutoff Unmet</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showCutoffUnmetIcon"
|
||||
value={showCutoffUnmetIcon}
|
||||
helpText="Show icon for files when the cutoff hasn't been met"
|
||||
onChange={this.onOptionInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</FieldSet>
|
||||
|
||||
<FieldSet legend="Global">
|
||||
<Form>
|
||||
<FormGroup>
|
||||
<FormLabel>First Day of Week</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="firstDayOfWeek"
|
||||
values={firstDayOfWeekOptions}
|
||||
value={firstDayOfWeek}
|
||||
onChange={this.onGlobalInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Week Column Header</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="calendarWeekColumnHeader"
|
||||
values={weekColumnOptions}
|
||||
value={calendarWeekColumnHeader}
|
||||
onChange={this.onGlobalInputChange}
|
||||
helpText="Shown above each column when week is the active view"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Time Format</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="timeFormat"
|
||||
values={timeFormatOptions}
|
||||
value={timeFormat}
|
||||
onChange={this.onGlobalInputChange}
|
||||
/>
|
||||
</FormGroup><FormGroup>
|
||||
<FormLabel>Enable Color-Impaired Mode</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enableColorImpairedMode"
|
||||
value={enableColorImpairedMode}
|
||||
helpText="Altered style to allow color-impaired users to better distinguish color coded information"
|
||||
onChange={this.onGlobalInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</Form>
|
||||
</FieldSet>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -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);
|
|
@ -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)
|
||||
|
|
|
@ -12,7 +12,7 @@ function createMapStateToProps() {
|
|||
(state) => state.settings.qualityProfiles,
|
||||
(qualityProfiles) => {
|
||||
const {
|
||||
isFetchingSchema: isFetching,
|
||||
isSchemaFetching: isFetching,
|
||||
isSchemaPopulated: isPopulated,
|
||||
schemaError: error,
|
||||
schema
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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 (
|
||||
<div className={className}>
|
||||
<Autosuggest
|
||||
id={name}
|
||||
inputProps={inputProps}
|
||||
theme={theme}
|
||||
suggestions={suggestions}
|
||||
getSuggestionValue={this.getSuggestionValue}
|
||||
renderSuggestion={this.renderSuggestion}
|
||||
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
|
||||
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -0,0 +1,3 @@
|
|||
.validationFailures {
|
||||
margin-bottom: 20px;
|
||||
}
|
|
@ -2,11 +2,14 @@ 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 (
|
||||
<div>
|
||||
<div>
|
||||
{
|
||||
validationErrors.length || validationWarnings.length ?
|
||||
<div className={styles.validationFailures}>
|
||||
{
|
||||
validationErrors.map((error, index) => {
|
||||
return (
|
||||
|
@ -32,7 +35,9 @@ function Form({ children, validationErrors, validationWarnings, ...otherProps })
|
|||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
}
|
|
@ -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 (
|
||||
<div className={classNames(
|
||||
className,
|
||||
isFocused && styles.isFocused
|
||||
)}
|
||||
>
|
||||
{
|
||||
[...value, { key: '', value: '' }].map((v, index) => {
|
||||
return (
|
||||
<KeyValueListInputItem
|
||||
key={index}
|
||||
index={index}
|
||||
keyValue={v.key}
|
||||
value={v.value}
|
||||
keyPlaceholder={keyPlaceholder}
|
||||
valuePlaceholder={valuePlaceholder}
|
||||
isNew={index === value.length}
|
||||
onChange={this.onItemChange}
|
||||
onRemove={this.onRemoveItem}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -0,0 +1,14 @@
|
|||
.itemContainer {
|
||||
display: flex;
|
||||
margin-bottom: 3px;
|
||||
border-bottom: 1px solid $inputBorderColor;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.keyInput,
|
||||
.valueInput {
|
||||
border: none;
|
||||
}
|
|
@ -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 (
|
||||
<div className={styles.itemContainer}>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
name="key"
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
onChange={this.onKeyChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
value={value}
|
||||
placeholder={valuePlaceholder}
|
||||
onChange={this.onValueChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
|
||||
{
|
||||
!isNew &&
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
|
@ -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,
|
||||
|
|
|
@ -2,34 +2,18 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import TextInput from './TextInput';
|
||||
|
||||
class NumberInput extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
let newValue = null;
|
||||
|
||||
if (value) {
|
||||
newValue = this.props.isFloat ? parseFloat(value) : parseInt(value);
|
||||
}
|
||||
|
||||
this.props.onChange({
|
||||
name,
|
||||
value: newValue
|
||||
});
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
function parseValue(props, value) {
|
||||
const {
|
||||
name,
|
||||
value,
|
||||
isFloat,
|
||||
min,
|
||||
max,
|
||||
onChange
|
||||
} = this.props;
|
||||
max
|
||||
} = props;
|
||||
|
||||
let newValue = value;
|
||||
if (value == null || value === '') {
|
||||
return min;
|
||||
}
|
||||
|
||||
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
||||
|
||||
if (min != null && newValue != null && newValue < min) {
|
||||
newValue = min;
|
||||
|
@ -37,9 +21,72 @@ class NumberInput extends Component {
|
|||
newValue = max;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
|
||||
class NumberInput extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
value: props.value == null ? '' : props.value.toString(),
|
||||
isFocused: false
|
||||
};
|
||||
}
|
||||
|
||||
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: parseValue(this.props, value)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onFocus = () => {
|
||||
this.setState({ isFocused: true });
|
||||
}
|
||||
|
||||
onBlur = () => {
|
||||
const {
|
||||
name,
|
||||
onChange
|
||||
} = this.props;
|
||||
|
||||
const { value } = this.state;
|
||||
const parsedValue = parseValue(this.props, value);
|
||||
const stringValue = parsedValue == null ? '' : parsedValue.toString();
|
||||
|
||||
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 (
|
||||
<TextInput
|
||||
{...this.props}
|
||||
type="number"
|
||||
value={value == null ? '' : value}
|
||||
{...otherProps}
|
||||
onChange={this.onChange}
|
||||
onBlur={this.onBlur}
|
||||
onFocus={this.onFocus}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -14,9 +14,7 @@
|
|||
}
|
||||
|
||||
.freeSpace {
|
||||
@add-mixin truncate;
|
||||
|
||||
flex: 1 0 0;
|
||||
flex: 0 0 auto;
|
||||
margin-left: 15px;
|
||||
color: $gray;
|
||||
text-align: right;
|
||||
|
|
|
@ -33,7 +33,10 @@ class TagInputTag extends Component {
|
|||
} = this.props;
|
||||
|
||||
return (
|
||||
<Link onPress={this.onDelete}>
|
||||
<Link
|
||||
tabIndex={-1}
|
||||
onPress={this.onDelete}
|
||||
>
|
||||
<Label kind={kind}>
|
||||
{tag.name}
|
||||
</Label>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue