mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-31 04:00:18 -07:00
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
409 changed files with 6882 additions and 3176 deletions
|
@ -177,7 +177,7 @@
|
||||||
"no-undef": "error",
|
"no-undef": "error",
|
||||||
"no-undef-init": "off",
|
"no-undef-init": "off",
|
||||||
"no-undefined": "off",
|
"no-undefined": "off",
|
||||||
"no-unused-vars": ["warn", { "args": "none" }],
|
"no-unused-vars": ["error", { "args": "none", "ignoreRestSiblings": true }],
|
||||||
"no-use-before-define": "error",
|
"no-use-before-define": "error",
|
||||||
|
|
||||||
# Node.js and CommonJS
|
# Node.js and CommonJS
|
||||||
|
@ -205,14 +205,13 @@
|
||||||
"func-style": ["error", "declaration"],
|
"func-style": ["error", "declaration"],
|
||||||
"indent": ["error", 2, {"SwitchCase": 1}],
|
"indent": ["error", 2, {"SwitchCase": 1}],
|
||||||
"key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
|
"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 }],
|
"lines-around-comment": ["error", { "beforeBlockComment": true, "afterBlockComment": false }],
|
||||||
"max-depth": ["error", {"maximum": 5}],
|
"max-depth": ["error", {"maximum": 5}],
|
||||||
"max-nested-callbacks": ["error", 4],
|
"max-nested-callbacks": ["error", 4],
|
||||||
"max-params": ["error", 7],
|
|
||||||
"max-statements": "off",
|
"max-statements": "off",
|
||||||
"max-statements-per-line": ["error", { "max": 1 }],
|
"max-statements-per-line": ["error", { "max": 1 }],
|
||||||
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred"]}],
|
"new-cap": ["error", {"capIsNewExceptions": ["$.Deferred", "DragDropContext", "DragLayer", "DragSource", "DropTarget"]}],
|
||||||
"new-parens": "error",
|
"new-parens": "error",
|
||||||
"newline-after-var": "off",
|
"newline-after-var": "off",
|
||||||
"newline-before-return": "off",
|
"newline-before-return": "off",
|
||||||
|
@ -223,7 +222,7 @@
|
||||||
"no-inline-comments": "off",
|
"no-inline-comments": "off",
|
||||||
"no-lonely-if": "warn",
|
"no-lonely-if": "warn",
|
||||||
"no-mixed-spaces-and-tabs": "error",
|
"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-negated-condition": "warn",
|
||||||
"no-nested-ternary": "error",
|
"no-nested-ternary": "error",
|
||||||
"no-new-object": "error",
|
"no-new-object": "error",
|
||||||
|
|
|
@ -14,6 +14,7 @@ module.exports = (ctx, configPath, options) => {
|
||||||
return Object.assign(acc, reload(vars));
|
return Object.assign(acc, reload(vars));
|
||||||
}, {})
|
}, {})
|
||||||
},
|
},
|
||||||
|
'postcss-color-function': {},
|
||||||
'postcss-nested': {},
|
'postcss-nested': {},
|
||||||
autoprefixer: {
|
autoprefixer: {
|
||||||
browsers: [
|
browsers: [
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
@ -41,6 +42,18 @@ class Blacklist extends Component {
|
||||||
onPress={onClearBlacklistPress}
|
onPress={onClearBlacklistPress}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Options"
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
<PageContentBodyConnector>
|
<PageContentBodyConnector>
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
import * as blacklistActions from 'Store/Actions/blacklistActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
@ -33,8 +34,19 @@ class BlacklistConnector extends Component {
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchBlacklist,
|
||||||
|
gotoBlacklistFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate);
|
||||||
this.props.gotoBlacklistFirstPage();
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchBlacklist();
|
||||||
|
} else {
|
||||||
|
gotoBlacklistFirstPage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -44,6 +56,7 @@ class BlacklistConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
this.props.clearBlacklist();
|
||||||
unregisterPagePopulator(this.repopulate);
|
unregisterPagePopulator(this.repopulate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +66,6 @@ class BlacklistConnector extends Component {
|
||||||
repopulate = () => {
|
repopulate = () => {
|
||||||
this.props.fetchBlacklist();
|
this.props.fetchBlacklist();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
@ -93,6 +105,14 @@ class BlacklistConnector extends Component {
|
||||||
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
this.props.executeCommand({ name: commandNames.CLEAR_BLACKLIST });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onTableOptionChange = (payload) => {
|
||||||
|
this.props.setBlacklistTableOption(payload);
|
||||||
|
|
||||||
|
if (payload.pageSize) {
|
||||||
|
this.props.gotoBlacklistFirstPage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -114,6 +134,7 @@ class BlacklistConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
BlacklistConnector.propTypes = {
|
BlacklistConnector.propTypes = {
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
isClearingBlacklistExecuting: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
fetchBlacklist: PropTypes.func.isRequired,
|
fetchBlacklist: PropTypes.func.isRequired,
|
||||||
|
@ -124,7 +145,10 @@ BlacklistConnector.propTypes = {
|
||||||
gotoBlacklistPage: PropTypes.func.isRequired,
|
gotoBlacklistPage: PropTypes.func.isRequired,
|
||||||
setBlacklistSort: PropTypes.func.isRequired,
|
setBlacklistSort: PropTypes.func.isRequired,
|
||||||
setBlacklistTableOption: PropTypes.func.isRequired,
|
setBlacklistTableOption: PropTypes.func.isRequired,
|
||||||
|
clearBlacklist: PropTypes.func.isRequired,
|
||||||
executeCommand: 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 RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
import TrackLanguage from 'Album/TrackLanguage';
|
||||||
import TrackQuality from 'Album/TrackQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
import BlacklistDetailsModal from './BlacklistDetailsModal';
|
||||||
|
@ -90,7 +90,7 @@ class BlacklistRow extends Component {
|
||||||
key={name}
|
key={name}
|
||||||
className={styles.language}
|
className={styles.language}
|
||||||
>
|
>
|
||||||
<EpisodeLanguage
|
<TrackLanguage
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
5
frontend/src/Activity/History/Details/HistoryDetails.css
Normal file
5
frontend/src/Activity/History/Details/HistoryDetails.css
Normal file
|
@ -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 DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||||
|
import styles from './HistoryDetails.css';
|
||||||
|
|
||||||
function getDetailedList(statusMessages) {
|
function getDetailedList(statusMessages) {
|
||||||
return (
|
return (
|
||||||
|
@ -60,6 +61,7 @@ function HistoryDetails(props) {
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
title="Name"
|
title="Name"
|
||||||
data={sourceTitle}
|
data={sourceTitle}
|
||||||
/>
|
/>
|
||||||
|
@ -75,6 +77,7 @@ function HistoryDetails(props) {
|
||||||
{
|
{
|
||||||
!!releaseGroup &&
|
!!releaseGroup &&
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
title="Release Group"
|
title="Release Group"
|
||||||
data={releaseGroup}
|
data={releaseGroup}
|
||||||
/>
|
/>
|
||||||
|
@ -136,6 +139,7 @@ function HistoryDetails(props) {
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
title="Name"
|
title="Name"
|
||||||
data={sourceTitle}
|
data={sourceTitle}
|
||||||
/>
|
/>
|
||||||
|
@ -160,6 +164,7 @@ function HistoryDetails(props) {
|
||||||
return (
|
return (
|
||||||
<DescriptionList>
|
<DescriptionList>
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
title="Name"
|
title="Name"
|
||||||
data={sourceTitle}
|
data={sourceTitle}
|
||||||
/>
|
/>
|
||||||
|
@ -167,6 +172,7 @@ function HistoryDetails(props) {
|
||||||
{
|
{
|
||||||
!!droppedPath &&
|
!!droppedPath &&
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
title="Source"
|
title="Source"
|
||||||
data={droppedPath}
|
data={droppedPath}
|
||||||
/>
|
/>
|
||||||
|
@ -175,6 +181,7 @@ function HistoryDetails(props) {
|
||||||
{
|
{
|
||||||
!!importedPath &&
|
!!importedPath &&
|
||||||
<DescriptionListItem
|
<DescriptionListItem
|
||||||
|
descriptionClassName={styles.description}
|
||||||
title="Imported To"
|
title="Imported To"
|
||||||
data={importedPath}
|
data={importedPath}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
import TableBody from 'Components/Table/TableBody';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
|
@ -75,6 +76,16 @@ class History extends Component {
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Options"
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
|
||||||
<FilterMenu
|
<FilterMenu
|
||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
import * as historyActions from 'Store/Actions/historyActions';
|
||||||
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
|
import { fetchAlbums, clearAlbums } from 'Store/Actions/albumActions';
|
||||||
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
||||||
|
@ -43,8 +44,19 @@ class HistoryConnector extends Component {
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchHistory,
|
||||||
|
gotoHistoryFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate);
|
||||||
this.props.gotoHistoryFirstPage();
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchHistory();
|
||||||
|
} else {
|
||||||
|
gotoHistoryFirstPage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
|
@ -138,6 +150,7 @@ class HistoryConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
HistoryConnector.propTypes = {
|
HistoryConnector.propTypes = {
|
||||||
|
useCurrentPage: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
fetchHistory: PropTypes.func.isRequired,
|
fetchHistory: PropTypes.func.isRequired,
|
||||||
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
gotoHistoryFirstPage: PropTypes.func.isRequired,
|
||||||
|
@ -155,4 +168,6 @@ HistoryConnector.propTypes = {
|
||||||
clearTracks: PropTypes.func.isRequired
|
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 TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
import TrackLanguage from 'Album/TrackLanguage';
|
||||||
import TrackQuality from 'Album/TrackQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
import HistoryEventTypeCell from './HistoryEventTypeCell';
|
||||||
|
@ -131,7 +131,7 @@ class HistoryRow extends Component {
|
||||||
if (name === 'language') {
|
if (name === 'language') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
<EpisodeLanguage
|
<TrackLanguage
|
||||||
language={language}
|
language={language}
|
||||||
isCutoffMet={languageCutoffNotMet}
|
isCutoffMet={languageCutoffNotMet}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,9 +3,10 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||||
|
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||||
import selectAll from 'Utilities/Table/selectAll';
|
import selectAll from 'Utilities/Table/selectAll';
|
||||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||||
import { icons } from 'Helpers/Props';
|
import { align, icons } from 'Helpers/Props';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import Table from 'Components/Table/Table';
|
import Table from 'Components/Table/Table';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
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 PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
|
||||||
|
import QueueOptionsConnector from './QueueOptionsConnector';
|
||||||
import QueueRowConnector from './QueueRowConnector';
|
import QueueRowConnector from './QueueRowConnector';
|
||||||
|
|
||||||
class Queue extends Component {
|
class Queue extends Component {
|
||||||
|
@ -42,22 +45,27 @@ class Queue extends Component {
|
||||||
// before albums start fetching or when albums start fetching.
|
// before albums start fetching or when albums start fetching.
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(
|
|
||||||
this.props.isFetching &&
|
this.props.isFetching &&
|
||||||
nextProps.isPopulated &&
|
nextProps.isPopulated &&
|
||||||
hasDifferentItems(this.props.items, nextProps.items)
|
hasDifferentItems(this.props.items, nextProps.items) &&
|
||||||
) ||
|
nextProps.items.some((e) => e.albumId)
|
||||||
(!this.props.isAlbumsFetching && nextProps.isAlbumsFetching)
|
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.props.isAlbumsFetching && nextProps.isAlbumsFetching) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
this.setState({ selectedState: {} });
|
this.setState((state) => {
|
||||||
|
return removeOldSelectedState(state, prevProps.items);
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -138,7 +146,7 @@ class Queue extends Component {
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const isRefreshing = isFetching || isAlbumsFetching || isCheckForFinishedDownloadExecuting;
|
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 hasError = error || albumsError;
|
||||||
const selectedCount = this.getSelectedIds().length;
|
const selectedCount = this.getSelectedIds().length;
|
||||||
const disableSelectedActions = selectedCount === 0;
|
const disableSelectedActions = selectedCount === 0;
|
||||||
|
@ -172,6 +180,21 @@ class Queue extends Component {
|
||||||
onPress={this.onRemoveSelectedPress}
|
onPress={this.onRemoveSelectedPress}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
|
<PageToolbarSection
|
||||||
|
alignContent={align.RIGHT}
|
||||||
|
>
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
{...otherProps}
|
||||||
|
optionsComponent={QueueOptionsConnector}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Options"
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
|
</PageToolbarSection>
|
||||||
</PageToolbar>
|
</PageToolbar>
|
||||||
|
|
||||||
<PageContentBodyConnector>
|
<PageContentBodyConnector>
|
||||||
|
@ -203,6 +226,7 @@ class Queue extends Component {
|
||||||
allSelected={allSelected}
|
allSelected={allSelected}
|
||||||
allUnselected={allUnselected}
|
allUnselected={allUnselected}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
optionsComponent={QueueOptionsConnector}
|
||||||
onSelectAllChange={this.onSelectAllChange}
|
onSelectAllChange={this.onSelectAllChange}
|
||||||
>
|
>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createSelector } from 'reselect';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||||
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as queueActions from 'Store/Actions/queueActions';
|
import * as queueActions from 'Store/Actions/queueActions';
|
||||||
|
@ -15,14 +16,16 @@ import Queue from './Queue';
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.albums,
|
(state) => state.albums,
|
||||||
|
(state) => state.queue.options,
|
||||||
(state) => state.queue.paged,
|
(state) => state.queue.paged,
|
||||||
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
||||||
(albums, queue, isCheckForFinishedDownloadExecuting) => {
|
(albums, options, queue, isCheckForFinishedDownloadExecuting) => {
|
||||||
return {
|
return {
|
||||||
isAlbumsFetching: albums.isFetching,
|
isAlbumsFetching: albums.isFetching,
|
||||||
isAlbumsPopulated: albums.isPopulated,
|
isAlbumsPopulated: albums.isPopulated,
|
||||||
albumsError: albums.error,
|
albumsError: albums.error,
|
||||||
isCheckForFinishedDownloadExecuting,
|
isCheckForFinishedDownloadExecuting,
|
||||||
|
...options,
|
||||||
...queue
|
...queue
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -42,19 +45,37 @@ class QueueConnector extends Component {
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchQueue,
|
||||||
|
gotoQueueFirstPage
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate);
|
||||||
this.props.gotoQueueFirstPage();
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchQueue();
|
||||||
|
} else {
|
||||||
|
gotoQueueFirstPage();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||||
const albumIds = selectUniqueIds(this.props.items, 'albumId');
|
const albumIds = selectUniqueIds(this.props.items, 'albumId');
|
||||||
|
|
||||||
if (albumIds.length) {
|
if (albumIds.length) {
|
||||||
this.props.fetchAlbums({ albumIds });
|
this.props.fetchAlbums({ albumIds });
|
||||||
} else {
|
} else {
|
||||||
this.props.clearAlbums();
|
this.props.clearAlbums();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this.props.includeUnknownArtistItems !==
|
||||||
|
prevProps.includeUnknownArtistItems
|
||||||
|
) {
|
||||||
|
this.repopulate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,4 +181,6 @@ QueueConnector.propTypes = {
|
||||||
executeCommand: PropTypes.func.isRequired
|
executeCommand: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(QueueConnector);
|
export default withCurrentPage(
|
||||||
|
connect(createMapStateToProps, mapDispatchToProps)(QueueConnector)
|
||||||
|
);
|
||||||
|
|
77
frontend/src/Activity/Queue/QueueOptions.js
Normal file
77
frontend/src/Activity/Queue/QueueOptions.js
Normal file
|
@ -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;
|
19
frontend/src/Activity/Queue/QueueOptionsConnector.js
Normal file
19
frontend/src/Activity/Queue/QueueOptionsConnector.js
Normal file
|
@ -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 Popover from 'Components/Tooltip/Popover';
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||||
|
import TrackLanguage from 'Album/TrackLanguage';
|
||||||
import TrackQuality from 'Album/TrackQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
|
||||||
import ArtistNameLink from 'Artist/ArtistNameLink';
|
import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
|
@ -72,6 +73,7 @@ class QueueRow extends Component {
|
||||||
errorMessage,
|
errorMessage,
|
||||||
artist,
|
artist,
|
||||||
album,
|
album,
|
||||||
|
language,
|
||||||
quality,
|
quality,
|
||||||
protocol,
|
protocol,
|
||||||
indexer,
|
indexer,
|
||||||
|
@ -137,21 +139,14 @@ class QueueRow extends Component {
|
||||||
if (name === 'artist.sortName') {
|
if (name === 'artist.sortName') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
artist ?
|
||||||
<ArtistNameLink
|
<ArtistNameLink
|
||||||
foreignArtistId={artist.foreignArtistId}
|
foreignArtistId={artist.foreignArtistId}
|
||||||
artistName={artist.artistName}
|
artistName={artist.artistName}
|
||||||
/>
|
/> :
|
||||||
</TableRowCell>
|
title
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'artist') {
|
|
||||||
return (
|
|
||||||
<TableRowCell key={name}>
|
|
||||||
<ArtistNameLink
|
|
||||||
foreignArtistId={artist.foreignArtistId}
|
|
||||||
artistName={artist.artistName}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -159,16 +154,21 @@ class QueueRow extends Component {
|
||||||
if (name === 'album.title') {
|
if (name === 'album.title') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
album ?
|
||||||
<AlbumTitleLink
|
<AlbumTitleLink
|
||||||
foreignAlbumId={album.foreignAlbumId}
|
foreignAlbumId={album.foreignAlbumId}
|
||||||
title={album.title}
|
title={album.title}
|
||||||
disambiguation={album.disambiguation}
|
disambiguation={album.disambiguation}
|
||||||
/>
|
/> :
|
||||||
|
'-'
|
||||||
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'album.releaseDate') {
|
if (name === 'album.releaseDate') {
|
||||||
|
if (album) {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCellConnector
|
||||||
key={name}
|
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') {
|
if (name === 'quality') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
|
@ -326,8 +343,9 @@ QueueRow.propTypes = {
|
||||||
trackedDownloadStatus: PropTypes.string,
|
trackedDownloadStatus: PropTypes.string,
|
||||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||||
errorMessage: PropTypes.string,
|
errorMessage: PropTypes.string,
|
||||||
artist: PropTypes.object.isRequired,
|
artist: PropTypes.object,
|
||||||
album: PropTypes.object.isRequired,
|
album: PropTypes.object,
|
||||||
|
language: PropTypes.object.isRequired,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
protocol: PropTypes.string.isRequired,
|
protocol: PropTypes.string.isRequired,
|
||||||
indexer: PropTypes.string,
|
indexer: PropTypes.string,
|
||||||
|
|
|
@ -51,10 +51,6 @@ class QueueRowConnector extends Component {
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.props.album) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueueRow
|
<QueueRow
|
||||||
{...this.props}
|
{...this.props}
|
||||||
|
|
|
@ -9,12 +9,19 @@ function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.app,
|
(state) => state.app,
|
||||||
(state) => state.queue.status,
|
(state) => state.queue.status,
|
||||||
(app, status) => {
|
(state) => state.queue.options.includeUnknownArtistItems,
|
||||||
|
(app, status, includeUnknownArtistItems) => {
|
||||||
|
const {
|
||||||
|
count,
|
||||||
|
unknownCount
|
||||||
|
} = status.item;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isConnected: app.isConnected,
|
isConnected: app.isConnected,
|
||||||
isReconnecting: app.isReconnecting,
|
isReconnecting: app.isReconnecting,
|
||||||
isPopulated: status.isPopulated,
|
isPopulated: status.isPopulated,
|
||||||
...status.item
|
...status.item,
|
||||||
|
count: includeUnknownArtistItems ? count : count - unknownCount
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -100,8 +100,8 @@ class AddNewArtist extends Component {
|
||||||
name="artistLookup"
|
name="artistLookup"
|
||||||
value={term}
|
value={term}
|
||||||
placeholder="eg. Breaking Benjamin, lidarr:854a1807-025b-42a8-ba8c-2a39717f1d25"
|
placeholder="eg. Breaking Benjamin, lidarr:854a1807-025b-42a8-ba8c-2a39717f1d25"
|
||||||
onChange={this.onSearchInputChange}
|
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
onChange={this.onSearchInputChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -74,7 +74,8 @@ class AddNewArtistModalContent extends Component {
|
||||||
showMetadataProfile,
|
showMetadataProfile,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
onModalClose,
|
onModalClose,
|
||||||
onInputChange
|
onInputChange,
|
||||||
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -86,7 +87,8 @@ class AddNewArtistModalContent extends Component {
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{
|
{
|
||||||
!isSmallScreen &&
|
isSmallScreen ?
|
||||||
|
null:
|
||||||
<div className={styles.poster}>
|
<div className={styles.poster}>
|
||||||
<ArtistPoster
|
<ArtistPoster
|
||||||
className={styles.poster}
|
className={styles.poster}
|
||||||
|
@ -97,15 +99,19 @@ class AddNewArtistModalContent extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
|
{
|
||||||
|
overview ?
|
||||||
<div className={styles.overview}>
|
<div className={styles.overview}>
|
||||||
<TextTruncate
|
<TextTruncate
|
||||||
truncateText="…"
|
truncateText="…"
|
||||||
line={8}
|
line={8}
|
||||||
text={overview}
|
text={overview}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
<Form>
|
<Form {...otherProps}>
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Root Folder</FormLabel>
|
<FormLabel>Root Folder</FormLabel>
|
||||||
|
|
||||||
|
|
|
@ -107,8 +107,11 @@ class AddNewArtistSearchResult extends Component {
|
||||||
{artistName}
|
{artistName}
|
||||||
|
|
||||||
{
|
{
|
||||||
!name.contains(year) && !!year &&
|
!name.contains(year) && year ?
|
||||||
<span className={styles.year}>({year})</span>
|
<span className={styles.year}>
|
||||||
|
({year})
|
||||||
|
</span> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -117,13 +120,14 @@ class AddNewArtistSearchResult extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isExistingArtist &&
|
isExistingArtist ?
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.alreadyExistsIcon}
|
className={styles.alreadyExistsIcon}
|
||||||
name={icons.CHECK_CIRCLE}
|
name={icons.CHECK_CIRCLE}
|
||||||
size={36}
|
size={36}
|
||||||
title="Already in your library"
|
title="Already in your library"
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -136,20 +140,22 @@ class AddNewArtistSearchResult extends Component {
|
||||||
</Label>
|
</Label>
|
||||||
|
|
||||||
{
|
{
|
||||||
!!artistType &&
|
artistType ?
|
||||||
<Label size={sizes.LARGE}>
|
<Label size={sizes.LARGE}>
|
||||||
{artistType}
|
{artistType}
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
status === 'ended' &&
|
status === 'ended' ?
|
||||||
<Label
|
<Label
|
||||||
kind={kinds.DANGER}
|
kind={kinds.DANGER}
|
||||||
size={sizes.LARGE}
|
size={sizes.LARGE}
|
||||||
>
|
>
|
||||||
Ended
|
Ended
|
||||||
</Label>
|
</Label> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -116,9 +116,11 @@ class ImportArtistFooter extends Component {
|
||||||
isQualityProfileIdMixed,
|
isQualityProfileIdMixed,
|
||||||
isLanguageProfileIdMixed,
|
isLanguageProfileIdMixed,
|
||||||
isMetadataProfileIdMixed,
|
isMetadataProfileIdMixed,
|
||||||
|
hasUnsearchedItems,
|
||||||
showLanguageProfile,
|
showLanguageProfile,
|
||||||
showMetadataProfile,
|
showMetadataProfile,
|
||||||
onImportPress,
|
onImportPress,
|
||||||
|
onLookupPress,
|
||||||
onCancelLookupPress
|
onCancelLookupPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -238,6 +240,17 @@ class ImportArtistFooter extends Component {
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
hasUnsearchedItems &&
|
||||||
|
<Button
|
||||||
|
className={styles.loadingButton}
|
||||||
|
kind={kinds.SUCCESS}
|
||||||
|
onPress={onLookupPress}
|
||||||
|
>
|
||||||
|
Start Processing
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isLookingUpArtist &&
|
isLookingUpArtist &&
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
|
@ -271,10 +284,12 @@ ImportArtistFooter.propTypes = {
|
||||||
isLanguageProfileIdMixed: PropTypes.bool.isRequired,
|
isLanguageProfileIdMixed: PropTypes.bool.isRequired,
|
||||||
isMetadataProfileIdMixed: PropTypes.bool.isRequired,
|
isMetadataProfileIdMixed: PropTypes.bool.isRequired,
|
||||||
isAlbumFolderMixed: PropTypes.bool.isRequired,
|
isAlbumFolderMixed: PropTypes.bool.isRequired,
|
||||||
|
hasUnsearchedItems: PropTypes.bool.isRequired,
|
||||||
showLanguageProfile: PropTypes.bool.isRequired,
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
showMetadataProfile: PropTypes.bool.isRequired,
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onImportPress: PropTypes.func.isRequired,
|
onImportPress: PropTypes.func.isRequired,
|
||||||
|
onLookupPress: PropTypes.func.isRequired,
|
||||||
onCancelLookupPress: PropTypes.func.isRequired
|
onCancelLookupPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import _ from 'lodash';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import ImportArtistFooter from './ImportArtistFooter';
|
import ImportArtistFooter from './ImportArtistFooter';
|
||||||
import { cancelLookupArtist } from 'Store/Actions/importArtistActions';
|
import { lookupUnsearchedArtist, cancelLookupArtist } from 'Store/Actions/importArtistActions';
|
||||||
|
|
||||||
function isMixed(items, selectedIds, defaultValue, key) {
|
function isMixed(items, selectedIds, defaultValue, key) {
|
||||||
return _.some(items, (artist) => {
|
return _.some(items, (artist) => {
|
||||||
|
@ -35,6 +35,7 @@ function createMapStateToProps() {
|
||||||
const isLanguageProfileIdMixed = isMixed(items, selectedIds, defaultLanguageProfileId, 'languageProfileId');
|
const isLanguageProfileIdMixed = isMixed(items, selectedIds, defaultLanguageProfileId, 'languageProfileId');
|
||||||
const isMetadataProfileIdMixed = isMixed(items, selectedIds, defaultMetadataProfileId, 'metadataProfileId');
|
const isMetadataProfileIdMixed = isMixed(items, selectedIds, defaultMetadataProfileId, 'metadataProfileId');
|
||||||
const isAlbumFolderMixed = isMixed(items, selectedIds, defaultAlbumFolder, 'albumFolder');
|
const isAlbumFolderMixed = isMixed(items, selectedIds, defaultAlbumFolder, 'albumFolder');
|
||||||
|
const hasUnsearchedItems = !isLookingUpArtist && items.some((item) => !item.isPopulated);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedCount: selectedIds.length,
|
selectedCount: selectedIds.length,
|
||||||
|
@ -49,13 +50,15 @@ function createMapStateToProps() {
|
||||||
isQualityProfileIdMixed,
|
isQualityProfileIdMixed,
|
||||||
isLanguageProfileIdMixed,
|
isLanguageProfileIdMixed,
|
||||||
isMetadataProfileIdMixed,
|
isMetadataProfileIdMixed,
|
||||||
isAlbumFolderMixed
|
isAlbumFolderMixed,
|
||||||
|
hasUnsearchedItems
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
|
onLookupPress: lookupUnsearchedArtist,
|
||||||
onCancelLookupPress: cancelLookupArtist
|
onCancelLookupPress: cancelLookupArtist
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,6 @@ ImportArtistRow.propTypes = {
|
||||||
selectedArtist: PropTypes.object,
|
selectedArtist: PropTypes.object,
|
||||||
isExistingArtist: PropTypes.bool.isRequired,
|
isExistingArtist: PropTypes.bool.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
queued: PropTypes.bool.isRequired,
|
|
||||||
showLanguageProfile: PropTypes.bool.isRequired,
|
showLanguageProfile: PropTypes.bool.isRequired,
|
||||||
showMetadataProfile: PropTypes.bool.isRequired,
|
showMetadataProfile: PropTypes.bool.isRequired,
|
||||||
isSelected: PropTypes.bool,
|
isSelected: PropTypes.bool,
|
||||||
|
|
|
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { queueLookupArtist, setImportArtistValue } from 'Store/Actions/importArtistActions';
|
import { setImportArtistValue } from 'Store/Actions/importArtistActions';
|
||||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||||
import ImportArtistRow from './ImportArtistRow';
|
import ImportArtistRow from './ImportArtistRow';
|
||||||
|
|
||||||
|
@ -34,7 +34,6 @@ function createMapStateToProps() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
queueLookupArtist,
|
|
||||||
setImportArtistValue
|
setImportArtistValue
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,7 +81,6 @@ ImportArtistRowConnector.propTypes = {
|
||||||
monitor: PropTypes.string,
|
monitor: PropTypes.string,
|
||||||
albumFolder: PropTypes.bool,
|
albumFolder: PropTypes.bool,
|
||||||
items: PropTypes.arrayOf(PropTypes.object),
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
queueLookupArtist: PropTypes.func.isRequired,
|
|
||||||
setImportArtistValue: PropTypes.func.isRequired
|
setImportArtistValue: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
.artistNameContainer {
|
.artistNameContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artistName {
|
.artistName {
|
||||||
margin-right: 5px;
|
@add-mixin truncate;
|
||||||
}
|
}
|
||||||
|
|
||||||
.disambiguation {
|
.disambiguation {
|
||||||
|
@ -12,11 +14,6 @@
|
||||||
color: $disabledColor;
|
color: $disabledColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.year {
|
|
||||||
margin-left: 5px;
|
|
||||||
color: $disabledColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.existing {
|
.existing {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ function ImportArtistName(props) {
|
||||||
const {
|
const {
|
||||||
artistName,
|
artistName,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
// year,
|
|
||||||
isExistingArtist
|
isExistingArtist
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
@ -36,7 +35,6 @@ function ImportArtistName(props) {
|
||||||
ImportArtistName.propTypes = {
|
ImportArtistName.propTypes = {
|
||||||
artistName: PropTypes.string.isRequired,
|
artistName: PropTypes.string.isRequired,
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
// year: PropTypes.number.isRequired,
|
|
||||||
isExistingArtist: PropTypes.bool.isRequired
|
isExistingArtist: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -30,8 +30,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdownArrowContainer {
|
.dropdownArrowContainer {
|
||||||
position: absolute;
|
flex: 1 0 auto;
|
||||||
right: 16px;
|
margin-left: 5px;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentContainer {
|
.contentContainer {
|
||||||
|
@ -68,3 +69,13 @@
|
||||||
|
|
||||||
border-radius: 0;
|
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,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items,
|
items,
|
||||||
queued,
|
isQueued,
|
||||||
isLookingUpArtist
|
isLookingUpArtist
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -142,7 +142,7 @@ class ImportArtistSelectArtist extends Component {
|
||||||
onPress={this.onPress}
|
onPress={this.onPress}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
isLookingUpArtist && queued && !isPopulated &&
|
isLookingUpArtist && isQueued && !isPopulated &&
|
||||||
<LoadingIndicator
|
<LoadingIndicator
|
||||||
className={styles.loading}
|
className={styles.loading}
|
||||||
size={20}
|
size={20}
|
||||||
|
@ -170,7 +170,7 @@ class ImportArtistSelectArtist extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !selectedArtist &&
|
isPopulated && !selectedArtist &&
|
||||||
<div>
|
<div className={styles.noMatches}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.warningIcon}
|
className={styles.warningIcon}
|
||||||
name={icons.WARNING}
|
name={icons.WARNING}
|
||||||
|
@ -265,7 +265,7 @@ ImportArtistSelectArtist.propTypes = {
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
queued: PropTypes.bool.isRequired,
|
isQueued: PropTypes.bool.isRequired,
|
||||||
isLookingUpArtist: PropTypes.bool.isRequired,
|
isLookingUpArtist: PropTypes.bool.isRequired,
|
||||||
onSearchInputChange: PropTypes.func.isRequired,
|
onSearchInputChange: PropTypes.func.isRequired,
|
||||||
onArtistSelect: PropTypes.func.isRequired
|
onArtistSelect: PropTypes.func.isRequired
|
||||||
|
@ -275,7 +275,7 @@ ImportArtistSelectArtist.defaultProps = {
|
||||||
isFetching: true,
|
isFetching: true,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
items: [],
|
items: [],
|
||||||
queued: true
|
isQueued: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImportArtistSelectArtist;
|
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 FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
import Table from 'Components/Table/Table';
|
import RootFolders from 'RootFolder/RootFolders';
|
||||||
import TableBody from 'Components/Table/TableBody';
|
|
||||||
import ImportArtistRootFolderRowConnector from './ImportArtistRootFolderRowConnector';
|
|
||||||
import styles from './ImportArtistSelectFolder.css';
|
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 {
|
class ImportArtistSelectFolder extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -107,26 +83,13 @@ class ImportArtistSelectFolder extends Component {
|
||||||
{
|
{
|
||||||
items.length > 0 ?
|
items.length > 0 ?
|
||||||
<div className={styles.recentFolders}>
|
<div className={styles.recentFolders}>
|
||||||
<FieldSet legend="Recent Folders">
|
<FieldSet legend="Root Folders">
|
||||||
<Table
|
<RootFolders
|
||||||
columns={rootFolderColumns}
|
isFetching={isFetching}
|
||||||
>
|
isPopulated={isPopulated}
|
||||||
<TableBody>
|
error={error}
|
||||||
{
|
items={items}
|
||||||
items.map((rootFolder) => {
|
|
||||||
return (
|
|
||||||
<ImportArtistRootFolderRowConnector
|
|
||||||
key={rootFolder.id}
|
|
||||||
id={rootFolder.id}
|
|
||||||
path={rootFolder.path}
|
|
||||||
freeSpace={rootFolder.freeSpace}
|
|
||||||
unmappedFolders={rootFolder.unmappedFolders}
|
|
||||||
/>
|
/>
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</FieldSet>
|
</FieldSet>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -178,8 +141,7 @@ ImportArtistSelectFolder.propTypes = {
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onNewRootFolderSelect: PropTypes.func.isRequired,
|
onNewRootFolderSelect: PropTypes.func.isRequired
|
||||||
onDeleteRootFolderPress: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ImportArtistSelectFolder;
|
export default ImportArtistSelectFolder;
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { push } from 'react-router-redux';
|
import { push } from 'react-router-redux';
|
||||||
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
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';
|
import ImportArtistSelectFolder from './ImportArtistSelectFolder';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -24,7 +24,6 @@ function createMapStateToProps() {
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
fetchRootFolders,
|
fetchRootFolders,
|
||||||
addRootFolder,
|
addRootFolder,
|
||||||
deleteRootFolder,
|
|
||||||
push
|
push
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -60,10 +59,6 @@ class ImportArtistSelectFolderConnector extends Component {
|
||||||
this.props.addRootFolder({ path });
|
this.props.addRootFolder({ path });
|
||||||
}
|
}
|
||||||
|
|
||||||
onDeleteRootFolderPress = (id) => {
|
|
||||||
this.props.deleteRootFolder({ id });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -72,7 +67,6 @@ class ImportArtistSelectFolderConnector extends Component {
|
||||||
<ImportArtistSelectFolder
|
<ImportArtistSelectFolder
|
||||||
{...this.props}
|
{...this.props}
|
||||||
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
onNewRootFolderSelect={this.onNewRootFolderSelect}
|
||||||
onDeleteRootFolderPress={this.onDeleteRootFolderPress}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -84,7 +78,6 @@ ImportArtistSelectFolderConnector.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
fetchRootFolders: PropTypes.func.isRequired,
|
fetchRootFolders: PropTypes.func.isRequired,
|
||||||
addRootFolder: PropTypes.func.isRequired,
|
addRootFolder: PropTypes.func.isRequired,
|
||||||
deleteRootFolder: PropTypes.func.isRequired,
|
|
||||||
push: PropTypes.func.isRequired
|
push: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { icons } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
import AlbumInteractiveSearchModalConnector from './Search/AlbumInteractiveSearchModalConnector';
|
||||||
import styles from './AlbumSearchCell.css';
|
import styles from './AlbumSearchCell.css';
|
||||||
|
|
||||||
class AlbumSearchCell extends Component {
|
class AlbumSearchCell extends Component {
|
||||||
|
@ -37,6 +37,7 @@ class AlbumSearchCell extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
albumId,
|
albumId,
|
||||||
|
albumTitle,
|
||||||
isSearching,
|
isSearching,
|
||||||
onSearchPress,
|
onSearchPress,
|
||||||
...otherProps
|
...otherProps
|
||||||
|
@ -55,9 +56,10 @@ class AlbumSearchCell extends Component {
|
||||||
onPress={this.onManualSearchPress}
|
onPress={this.onManualSearchPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InteractiveSearchModal
|
<AlbumInteractiveSearchModalConnector
|
||||||
isOpen={this.state.isDetailsModalOpen}
|
isOpen={this.state.isDetailsModalOpen}
|
||||||
albumId={albumId}
|
albumId={albumId}
|
||||||
|
albumTitle={albumTitle}
|
||||||
onModalClose={this.onDetailsModalClose}
|
onModalClose={this.onDetailsModalClose}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -32,13 +32,6 @@
|
||||||
color: $white;
|
color: $white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
flex-shrink: 0;
|
|
||||||
margin-right: 35px;
|
|
||||||
width: 250px;
|
|
||||||
height: 97px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cover {
|
.cover {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-right: 35px;
|
margin-right: 35px;
|
||||||
|
@ -61,19 +54,33 @@
|
||||||
|
|
||||||
.titleContainer {
|
.titleContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-bottom: 5px;
|
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 50px;
|
font-size: 50px;
|
||||||
line-height: 50px;
|
line-height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toggleMonitoredContainer {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monitorToggleButton {
|
||||||
|
composes: toggleButton from 'Components/MonitorToggleButton.css';
|
||||||
|
|
||||||
|
width: 40px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.alternateTitlesIconContainer {
|
.alternateTitlesIconContainer {
|
||||||
|
align-self: flex-end;
|
||||||
margin-left: 20px;
|
margin-left: 20px;
|
||||||
line-height: 50px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.albumNavigationButtons {
|
.albumNavigationButtons {
|
||||||
|
@ -87,14 +94,19 @@
|
||||||
width: 30px;
|
width: 30px;
|
||||||
color: #e1e2e3;
|
color: #e1e2e3;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $iconButtonHoverLightColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
|
margin-bottom: 8px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.runtime {
|
.duration {
|
||||||
margin-right: 15px;
|
margin-right: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,7 +127,9 @@
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
margin-top: 8px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
font-size: $intermediateFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentContainer {
|
.contentContainer {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import AlbumCover from 'Album/AlbumCover';
|
import AlbumCover from 'Album/AlbumCover';
|
||||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||||
|
@ -25,7 +26,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
|
import AlbumDetailsMediumConnector from './AlbumDetailsMediumConnector';
|
||||||
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||||
import InteractiveSearchModal from 'InteractiveSearch/InteractiveSearchModal';
|
import AlbumInteractiveSearchModalConnector from 'Album/Search/AlbumInteractiveSearchModalConnector';
|
||||||
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
||||||
import AlbumDetailsLinks from './AlbumDetailsLinks';
|
import AlbumDetailsLinks from './AlbumDetailsLinks';
|
||||||
import styles from './AlbumDetails.css';
|
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) {
|
function getExpandedState(newState) {
|
||||||
return {
|
return {
|
||||||
allExpanded: newState.allSelected,
|
allExpanded: newState.allSelected,
|
||||||
|
@ -144,6 +167,7 @@ class AlbumDetails extends Component {
|
||||||
foreignAlbumId,
|
foreignAlbumId,
|
||||||
title,
|
title,
|
||||||
disambiguation,
|
disambiguation,
|
||||||
|
duration,
|
||||||
overview,
|
overview,
|
||||||
albumType,
|
albumType,
|
||||||
statistics = {},
|
statistics = {},
|
||||||
|
@ -153,6 +177,7 @@ class AlbumDetails extends Component {
|
||||||
images,
|
images,
|
||||||
links,
|
links,
|
||||||
media,
|
media,
|
||||||
|
isSaving,
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
|
@ -162,6 +187,7 @@ class AlbumDetails extends Component {
|
||||||
previousAlbum,
|
previousAlbum,
|
||||||
nextAlbum,
|
nextAlbum,
|
||||||
isSearching,
|
isSearching,
|
||||||
|
onMonitorTogglePress,
|
||||||
onSearchPress
|
onSearchPress
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -259,10 +285,23 @@ class AlbumDetails extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.info}>
|
<div className={styles.info}>
|
||||||
|
<div className={styles.titleRow}>
|
||||||
<div className={styles.titleContainer}>
|
<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}>
|
<div className={styles.title}>
|
||||||
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
{title}{disambiguation ? ` (${disambiguation})` : ''}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.albumNavigationButtons}>
|
<div className={styles.albumNavigationButtons}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -293,6 +332,13 @@ class AlbumDetails extends Component {
|
||||||
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
<div>
|
<div>
|
||||||
|
{
|
||||||
|
!!duration &&
|
||||||
|
<span className={styles.duration}>
|
||||||
|
{formatDuration(duration)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
|
||||||
<HeartRating
|
<HeartRating
|
||||||
rating={ratings.value}
|
rating={ratings.value}
|
||||||
iconSize={20}
|
iconSize={20}
|
||||||
|
@ -456,9 +502,10 @@ class AlbumDetails extends Component {
|
||||||
onModalClose={this.onManageTracksModalClose}
|
onModalClose={this.onManageTracksModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InteractiveSearchModal
|
<AlbumInteractiveSearchModalConnector
|
||||||
isOpen={isInteractiveSearchModalOpen}
|
isOpen={isInteractiveSearchModalOpen}
|
||||||
albumId={id}
|
albumId={id}
|
||||||
|
albumTitle={title}
|
||||||
onModalClose={this.onInteractiveSearchModalClose}
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -487,6 +534,7 @@ AlbumDetails.propTypes = {
|
||||||
foreignAlbumId: PropTypes.string.isRequired,
|
foreignAlbumId: PropTypes.string.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
disambiguation: PropTypes.string,
|
disambiguation: PropTypes.string,
|
||||||
|
duration: PropTypes.number,
|
||||||
overview: PropTypes.string,
|
overview: PropTypes.string,
|
||||||
albumType: PropTypes.string.isRequired,
|
albumType: PropTypes.string.isRequired,
|
||||||
statistics: PropTypes.object.isRequired,
|
statistics: PropTypes.object.isRequired,
|
||||||
|
@ -497,6 +545,7 @@ AlbumDetails.propTypes = {
|
||||||
media: PropTypes.arrayOf(PropTypes.object).isRequired,
|
media: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
|
isSaving: PropTypes.bool.isRequired,
|
||||||
isSearching: PropTypes.bool,
|
isSearching: PropTypes.bool,
|
||||||
isFetching: PropTypes.bool,
|
isFetching: PropTypes.bool,
|
||||||
isPopulated: PropTypes.bool,
|
isPopulated: PropTypes.bool,
|
||||||
|
@ -506,6 +555,7 @@ AlbumDetails.propTypes = {
|
||||||
artist: PropTypes.object,
|
artist: PropTypes.object,
|
||||||
previousAlbum: PropTypes.object,
|
previousAlbum: PropTypes.object,
|
||||||
nextAlbum: PropTypes.object,
|
nextAlbum: PropTypes.object,
|
||||||
|
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||||
onRefreshPress: PropTypes.func,
|
onRefreshPress: PropTypes.func,
|
||||||
onSearchPress: PropTypes.func.isRequired
|
onSearchPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,6 +7,7 @@ import { createSelector } from 'reselect';
|
||||||
import { findCommand } from 'Utilities/Command';
|
import { findCommand } from 'Utilities/Command';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
|
import { toggleAlbumsMonitored } from 'Store/Actions/albumActions';
|
||||||
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
||||||
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
|
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
|
@ -64,7 +65,8 @@ const mapDispatchToProps = {
|
||||||
fetchTracks,
|
fetchTracks,
|
||||||
clearTracks,
|
clearTracks,
|
||||||
fetchTrackFiles,
|
fetchTrackFiles,
|
||||||
clearTrackFiles
|
clearTrackFiles,
|
||||||
|
toggleAlbumsMonitored
|
||||||
};
|
};
|
||||||
|
|
||||||
function getMonitoredReleases(props) {
|
function getMonitoredReleases(props) {
|
||||||
|
@ -109,6 +111,13 @@ class AlbumDetailsConnector extends Component {
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
onMonitorTogglePress = (monitored) => {
|
||||||
|
this.props.toggleAlbumsMonitored({
|
||||||
|
albumIds: [this.props.id],
|
||||||
|
monitored
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onSearchPress = () => {
|
onSearchPress = () => {
|
||||||
this.props.executeCommand({
|
this.props.executeCommand({
|
||||||
name: commandNames.ALBUM_SEARCH,
|
name: commandNames.ALBUM_SEARCH,
|
||||||
|
@ -123,6 +132,7 @@ class AlbumDetailsConnector extends Component {
|
||||||
return (
|
return (
|
||||||
<AlbumDetails
|
<AlbumDetails
|
||||||
{...this.props}
|
{...this.props}
|
||||||
|
onMonitorTogglePress={this.onMonitorTogglePress}
|
||||||
onSearchPress={this.onSearchPress}
|
onSearchPress={this.onSearchPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -138,6 +148,7 @@ AlbumDetailsConnector.propTypes = {
|
||||||
clearTracks: PropTypes.func.isRequired,
|
clearTracks: PropTypes.func.isRequired,
|
||||||
fetchTrackFiles: PropTypes.func.isRequired,
|
fetchTrackFiles: PropTypes.func.isRequired,
|
||||||
clearTrackFiles: PropTypes.func.isRequired,
|
clearTrackFiles: PropTypes.func.isRequired,
|
||||||
|
toggleAlbumsMonitored: PropTypes.func.isRequired,
|
||||||
executeCommand: PropTypes.func.isRequired
|
executeCommand: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
36
frontend/src/Album/Search/AlbumInteractiveSearchModal.js
Normal file
36
frontend/src/Album/Search/AlbumInteractiveSearchModal.js
Normal file
|
@ -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 Label from 'Components/Label';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
|
|
||||||
function EpisodeLanguage(props) {
|
function TrackLanguage(props) {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
language,
|
language,
|
||||||
|
@ -24,14 +24,14 @@ function EpisodeLanguage(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpisodeLanguage.propTypes = {
|
TrackLanguage.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
language: PropTypes.object,
|
language: PropTypes.object,
|
||||||
isCutoffNotMet: PropTypes.bool
|
isCutoffNotMet: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeLanguage.defaultProps = {
|
TrackLanguage.defaultProps = {
|
||||||
isCutoffNotMet: true
|
isCutoffNotMet: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EpisodeLanguage;
|
export default TrackLanguage;
|
|
@ -12,6 +12,7 @@ function AppUpdatedModal(props) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
closeOnBackgroundClick={false}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<AppUpdatedModalContentConnector
|
<AppUpdatedModalContentConnector
|
||||||
|
|
6
frontend/src/App/ColorImpairedContext.js
Normal file
6
frontend/src/App/ColorImpairedContext.js
Normal file
|
@ -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 PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import LazyLoad from 'react-lazyload';
|
import ArtistImage from './ArtistImage';
|
||||||
|
|
||||||
const bannerPlaceholder = 'data:image/png;base64,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';
|
const bannerPlaceholder = 'data:image/png;base64,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';
|
||||||
|
|
||||||
function findBanner(images) {
|
function ArtistBanner(props) {
|
||||||
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) {
|
|
||||||
return (
|
return (
|
||||||
<img
|
<ArtistImage
|
||||||
className={className}
|
{...props}
|
||||||
style={style}
|
coverType="banner"
|
||||||
src={bannerPlaceholder}
|
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 = {
|
ArtistBanner.propTypes = {
|
||||||
className: PropTypes.string,
|
size: PropTypes.number.isRequired
|
||||||
style: PropTypes.object,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
lazy: PropTypes.bool.isRequired,
|
|
||||||
overflow: PropTypes.bool.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtistBanner.defaultProps = {
|
ArtistBanner.defaultProps = {
|
||||||
size: 70,
|
size: 70
|
||||||
lazy: true,
|
|
||||||
overflow: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistBanner;
|
export default ArtistBanner;
|
||||||
|
|
199
frontend/src/Artist/ArtistImage.js
Normal file
199
frontend/src/Artist/ArtistImage.js
Normal file
|
@ -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 PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
import LazyLoad from 'react-lazyload';
|
import ArtistImage from './ArtistImage';
|
||||||
|
|
||||||
const posterPlaceholder = 'data:image/png;base64,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';
|
const posterPlaceholder = 'data:image/png;base64,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';
|
||||||
|
|
||||||
function findPoster(images) {
|
function ArtistPoster(props) {
|
||||||
return _.find(images, { coverType: 'poster' });
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPosterUrl(poster, size) {
|
|
||||||
if (poster) {
|
|
||||||
if (poster.url.contains('lastWrite=') || (/^https?:/).test(poster.url)) {
|
|
||||||
// Remove protocol
|
|
||||||
let url = poster.url.replace(/^https?:/, '');
|
|
||||||
url = url.replace('poster.jpg', `poster-${size}.jpg`);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ArtistPoster extends Component {
|
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
const pixelRatio = Math.floor(window.devicePixelRatio);
|
|
||||||
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const poster = findPoster(images);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
pixelRatio,
|
|
||||||
poster,
|
|
||||||
posterUrl: getPosterUrl(poster, pixelRatio * size),
|
|
||||||
isLoaded: false,
|
|
||||||
hasError: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
const {
|
|
||||||
images,
|
|
||||||
size
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
poster,
|
|
||||||
pixelRatio
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
const nextPoster = findPoster(images);
|
|
||||||
|
|
||||||
if (nextPoster && (!poster || nextPoster.url !== poster.url)) {
|
|
||||||
this.setState({
|
|
||||||
poster: nextPoster,
|
|
||||||
posterUrl: getPosterUrl(nextPoster, pixelRatio * size),
|
|
||||||
hasError: false,
|
|
||||||
isLoaded: true
|
|
||||||
});
|
|
||||||
} else if (!nextPoster && poster) {
|
|
||||||
this.setState({
|
|
||||||
poster: nextPoster,
|
|
||||||
posterUrl: posterPlaceholder,
|
|
||||||
hasError: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onError = () => {
|
|
||||||
this.setState({ hasError: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoad = () => {
|
|
||||||
this.setState({
|
|
||||||
isLoaded: true,
|
|
||||||
hasError: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
style,
|
|
||||||
size,
|
|
||||||
lazy,
|
|
||||||
overflow
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
posterUrl,
|
|
||||||
hasError,
|
|
||||||
isLoaded
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
if (hasError || !posterUrl) {
|
|
||||||
return (
|
return (
|
||||||
<img
|
<ArtistImage
|
||||||
className={className}
|
{...props}
|
||||||
style={style}
|
coverType="poster"
|
||||||
src={posterPlaceholder}
|
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 = {
|
ArtistPoster.propTypes = {
|
||||||
className: PropTypes.string,
|
size: PropTypes.number.isRequired
|
||||||
style: PropTypes.object,
|
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
size: PropTypes.number.isRequired,
|
|
||||||
lazy: PropTypes.bool.isRequired,
|
|
||||||
overflow: PropTypes.bool.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtistPoster.defaultProps = {
|
ArtistPoster.defaultProps = {
|
||||||
size: 250,
|
size: 250
|
||||||
lazy: true,
|
|
||||||
overflow: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistPoster;
|
export default ArtistPoster;
|
||||||
|
|
|
@ -108,6 +108,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.details {
|
.details {
|
||||||
|
margin-bottom: 8px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
}
|
}
|
||||||
|
@ -132,15 +133,11 @@
|
||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path {
|
|
||||||
vertical-align: text-top;
|
|
||||||
font-size: $defaultFontSize;
|
|
||||||
font-family: $monoSpaceFontFamily;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
margin-top: 8px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
font-size: $intermediateFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contentContainer {
|
.contentContainer {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import HeartRating from 'Components/HeartRating';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import Measure from 'Components/Measure';
|
|
||||||
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
import MonitorToggleButton from 'Components/MonitorToggleButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
|
@ -35,6 +34,7 @@ import ArtistTagsConnector from './ArtistTagsConnector';
|
||||||
import ArtistDetailsLinks from './ArtistDetailsLinks';
|
import ArtistDetailsLinks from './ArtistDetailsLinks';
|
||||||
import styles from './ArtistDetails.css';
|
import styles from './ArtistDetails.css';
|
||||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||||
|
import ArtistInteractiveSearchModalConnector from 'Artist/Search/ArtistInteractiveSearchModalConnector';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
|
||||||
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||||
|
@ -71,6 +71,7 @@ class ArtistDetails extends Component {
|
||||||
isDeleteArtistModalOpen: false,
|
isDeleteArtistModalOpen: false,
|
||||||
isArtistHistoryModalOpen: false,
|
isArtistHistoryModalOpen: false,
|
||||||
isInteractiveImportModalOpen: false,
|
isInteractiveImportModalOpen: false,
|
||||||
|
isInteractiveSearchModalOpen: false,
|
||||||
allExpanded: false,
|
allExpanded: false,
|
||||||
allCollapsed: false,
|
allCollapsed: false,
|
||||||
expandedState: {}
|
expandedState: {}
|
||||||
|
@ -104,6 +105,14 @@ class ArtistDetails extends Component {
|
||||||
this.setState({ isInteractiveImportModalOpen: false });
|
this.setState({ isInteractiveImportModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onInteractiveSearchPress = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onInteractiveSearchModalClose = () => {
|
||||||
|
this.setState({ isInteractiveSearchModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
onEditArtistPress = () => {
|
onEditArtistPress = () => {
|
||||||
this.setState({ isEditArtistModalOpen: true });
|
this.setState({ isEditArtistModalOpen: true });
|
||||||
}
|
}
|
||||||
|
@ -181,7 +190,9 @@ class ArtistDetails extends Component {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
|
hasAlbums,
|
||||||
hasMonitoredAlbums,
|
hasMonitoredAlbums,
|
||||||
|
hasTrackFiles,
|
||||||
previousArtist,
|
previousArtist,
|
||||||
nextArtist,
|
nextArtist,
|
||||||
onMonitorTogglePress,
|
onMonitorTogglePress,
|
||||||
|
@ -201,6 +212,7 @@ class ArtistDetails extends Component {
|
||||||
isDeleteArtistModalOpen,
|
isDeleteArtistModalOpen,
|
||||||
isArtistHistoryModalOpen,
|
isArtistHistoryModalOpen,
|
||||||
isInteractiveImportModalOpen,
|
isInteractiveImportModalOpen,
|
||||||
|
isInteractiveSearchModalOpen,
|
||||||
allExpanded,
|
allExpanded,
|
||||||
allCollapsed,
|
allCollapsed,
|
||||||
expandedState
|
expandedState
|
||||||
|
@ -240,29 +252,41 @@ class ArtistDetails extends Component {
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Search Monitored"
|
label="Search Monitored"
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
isDisabled={!monitored || !hasMonitoredAlbums}
|
isDisabled={!monitored || !hasMonitoredAlbums || !hasAlbums}
|
||||||
isSpinning={isSearching}
|
isSpinning={isSearching}
|
||||||
title={hasMonitoredAlbums ? undefined : 'No monitored albums for this artist'}
|
title={hasMonitoredAlbums ? undefined : 'No monitored albums for this artist'}
|
||||||
onPress={onSearchPress}
|
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 />
|
<PageToolbarSeparator />
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Preview Rename"
|
label="Preview Rename"
|
||||||
iconName={icons.ORGANIZE}
|
iconName={icons.ORGANIZE}
|
||||||
|
isDisabled={!hasTrackFiles}
|
||||||
onPress={this.onOrganizePress}
|
onPress={this.onOrganizePress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Manage Tracks"
|
label="Manage Tracks"
|
||||||
iconName={icons.TRACK_FILE}
|
iconName={icons.TRACK_FILE}
|
||||||
|
isDisabled={!hasTrackFiles}
|
||||||
onPress={this.onManageTracksPress}
|
onPress={this.onManageTracksPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="History"
|
label="History"
|
||||||
iconName={icons.HISTORY}
|
iconName={icons.HISTORY}
|
||||||
|
isDisabled={!hasAlbums}
|
||||||
onPress={this.onArtistHistoryPress}
|
onPress={this.onArtistHistoryPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -609,6 +633,12 @@ class ArtistDetails extends Component {
|
||||||
showImportMode={false}
|
showImportMode={false}
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ArtistInteractiveSearchModalConnector
|
||||||
|
isOpen={isInteractiveSearchModalOpen}
|
||||||
|
artistId={id}
|
||||||
|
onModalClose={this.onInteractiveSearchModalClose}
|
||||||
|
/>
|
||||||
</PageContentBodyConnector>
|
</PageContentBodyConnector>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
|
@ -638,7 +668,9 @@ ArtistDetails.propTypes = {
|
||||||
isPopulated: PropTypes.bool.isRequired,
|
isPopulated: PropTypes.bool.isRequired,
|
||||||
albumsError: PropTypes.object,
|
albumsError: PropTypes.object,
|
||||||
trackFilesError: PropTypes.object,
|
trackFilesError: PropTypes.object,
|
||||||
|
hasAlbums: PropTypes.bool.isRequired,
|
||||||
hasMonitoredAlbums: PropTypes.bool.isRequired,
|
hasMonitoredAlbums: PropTypes.bool.isRequired,
|
||||||
|
hasTrackFiles: PropTypes.bool.isRequired,
|
||||||
previousArtist: PropTypes.object.isRequired,
|
previousArtist: PropTypes.object.isRequired,
|
||||||
nextArtist: PropTypes.object.isRequired,
|
nextArtist: PropTypes.object.isRequired,
|
||||||
onMonitorTogglePress: PropTypes.func.isRequired,
|
onMonitorTogglePress: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -16,11 +16,55 @@ import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import ArtistDetails from './ArtistDetails';
|
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() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { foreignArtistId }) => foreignArtistId,
|
(state, { foreignArtistId }) => foreignArtistId,
|
||||||
(state) => state.albums,
|
selectAlbums,
|
||||||
(state) => state.trackFiles,
|
selectTrackFiles,
|
||||||
(state) => state.settings.metadataProfiles,
|
(state) => state.settings.metadataProfiles,
|
||||||
createAllArtistSelector(),
|
createAllArtistSelector(),
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
|
@ -40,6 +84,21 @@ function createMapStateToProps() {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
isAlbumsFetching,
|
||||||
|
isAlbumsPopulated,
|
||||||
|
albumsError,
|
||||||
|
hasAlbums,
|
||||||
|
hasMonitoredAlbums
|
||||||
|
} = albums;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isTrackFilesFetching,
|
||||||
|
isTrackFilesPopulated,
|
||||||
|
trackFilesError,
|
||||||
|
hasTrackFiles
|
||||||
|
} = trackFiles;
|
||||||
|
|
||||||
const sortedAlbumTypes = _.orderBy(albumTypes);
|
const sortedAlbumTypes = _.orderBy(albumTypes);
|
||||||
|
|
||||||
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
|
const previousArtist = sortedArtist[artistIndex - 1] || _.last(sortedArtist);
|
||||||
|
@ -60,10 +119,9 @@ function createMapStateToProps() {
|
||||||
isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1
|
isRenamingArtistCommand.body.artistIds.indexOf(artist.id) > -1
|
||||||
);
|
);
|
||||||
|
|
||||||
const isFetching = albums.isFetching || trackFiles.isFetching;
|
const isFetching = isAlbumsFetching || isTrackFilesFetching;
|
||||||
const isPopulated = albums.isPopulated && trackFiles.isPopulated;
|
const isPopulated = isAlbumsPopulated && isTrackFilesPopulated;
|
||||||
const albumsError = albums.error;
|
|
||||||
const trackFilesError = trackFiles.error;
|
|
||||||
const alternateTitles = _.reduce(artist.alternateTitles, (acc, alternateTitle) => {
|
const alternateTitles = _.reduce(artist.alternateTitles, (acc, alternateTitle) => {
|
||||||
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
|
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
|
||||||
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) {
|
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) {
|
||||||
|
@ -73,8 +131,6 @@ function createMapStateToProps() {
|
||||||
return acc;
|
return acc;
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const hasMonitoredAlbums = albums.items.some((e) => e.monitored);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...artist,
|
...artist,
|
||||||
albumTypes: sortedAlbumTypes,
|
albumTypes: sortedAlbumTypes,
|
||||||
|
@ -89,7 +145,9 @@ function createMapStateToProps() {
|
||||||
isPopulated,
|
isPopulated,
|
||||||
albumsError,
|
albumsError,
|
||||||
trackFilesError,
|
trackFilesError,
|
||||||
|
hasAlbums,
|
||||||
hasMonitoredAlbums,
|
hasMonitoredAlbums,
|
||||||
|
hasTrackFiles,
|
||||||
previousArtist,
|
previousArtist,
|
||||||
nextArtist
|
nextArtist
|
||||||
};
|
};
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
composes: menuContent from 'Components/Menu/MenuContent.css';
|
composes: menuContent from 'Components/Menu/MenuContent.css';
|
||||||
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: $defaultFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actionMenuIcon {
|
.actionMenuIcon {
|
||||||
|
|
|
@ -34,8 +34,13 @@ class ArtistDetailsSeason extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (prevProps.artistId !== this.props.artistId) {
|
const {
|
||||||
|
artistId
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (prevProps.artistId !== artistId) {
|
||||||
this._expandByDefault();
|
this._expandByDefault();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +56,7 @@ class ArtistDetailsSeason extends Component {
|
||||||
|
|
||||||
const expand = _.some(items, (item) => {
|
const expand = _.some(items, (item) => {
|
||||||
return isAfter(item.releaseDate) ||
|
return isAfter(item.releaseDate) ||
|
||||||
isAfter(item.releaseDate, { days: -30 });
|
isAfter(item.releaseDate, { days: -365 });
|
||||||
});
|
});
|
||||||
|
|
||||||
onExpandPress(name, expand);
|
onExpandPress(name, expand);
|
||||||
|
@ -113,7 +118,6 @@ class ArtistDetailsSeason extends Component {
|
||||||
items,
|
items,
|
||||||
columns,
|
columns,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
artistMonitored,
|
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
onSortPress,
|
onSortPress,
|
||||||
|
@ -235,7 +239,6 @@ ArtistDetailsSeason.propTypes = {
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isExpanded: PropTypes.bool,
|
isExpanded: PropTypes.bool,
|
||||||
artistMonitored: PropTypes.bool.isRequired,
|
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired,
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
onExpandPress: PropTypes.func.isRequired,
|
onExpandPress: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -4,14 +4,12 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { toggleAlbumsMonitored, setAlbumsTableOption, setAlbumsSort } from 'Store/Actions/albumActions';
|
import { toggleAlbumsMonitored, setAlbumsTableOption, setAlbumsSort } from 'Store/Actions/albumActions';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
|
||||||
import ArtistDetailsSeason from './ArtistDetailsSeason';
|
import ArtistDetailsSeason from './ArtistDetailsSeason';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
|
|
@ -85,9 +85,7 @@ class EditArtistModalContent extends Component {
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<Form
|
<Form {...otherProps}>
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>Monitored</FormLabel>
|
<FormLabel>Monitored</FormLabel>
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
import TrackLanguage from 'Album/TrackLanguage';
|
||||||
import TrackQuality from 'Album/TrackQuality';
|
import TrackQuality from 'Album/TrackQuality';
|
||||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
|
@ -68,8 +68,6 @@ class ArtistHistoryRow extends Component {
|
||||||
qualityCutoffNotMet,
|
qualityCutoffNotMet,
|
||||||
date,
|
date,
|
||||||
data,
|
data,
|
||||||
fullArtist,
|
|
||||||
artist,
|
|
||||||
album
|
album
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -93,7 +91,7 @@ class ArtistHistoryRow extends Component {
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
<EpisodeLanguage
|
<TrackLanguage
|
||||||
language={language}
|
language={language}
|
||||||
isCutoffNotMet={languageCutoffNotMet}
|
isCutoffNotMet={languageCutoffNotMet}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,12 +7,14 @@ import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||||
import PageJumpBar from 'Components/Page/PageJumpBar';
|
import PageJumpBar from 'Components/Page/PageJumpBar';
|
||||||
|
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import NoArtist from 'Artist/NoArtist';
|
import NoArtist from 'Artist/NoArtist';
|
||||||
import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector';
|
import ArtistIndexTableConnector from './Table/ArtistIndexTableConnector';
|
||||||
|
import ArtistIndexTableOptionsConnector from './Table/ArtistIndexTableOptionsConnector';
|
||||||
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
|
import ArtistIndexPosterOptionsModal from './Posters/Options/ArtistIndexPosterOptionsModal';
|
||||||
import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector';
|
import ArtistIndexPostersConnector from './Posters/ArtistIndexPostersConnector';
|
||||||
import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal';
|
import ArtistIndexBannerOptionsModal from './Banners/Options/ArtistIndexBannerOptionsModal';
|
||||||
|
@ -187,6 +189,7 @@ class ArtistIndex extends Component {
|
||||||
error,
|
error,
|
||||||
totalItems,
|
totalItems,
|
||||||
items,
|
items,
|
||||||
|
columns,
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
customFilters,
|
customFilters,
|
||||||
|
@ -245,35 +248,52 @@ class ArtistIndex extends Component {
|
||||||
alignContent={align.RIGHT}
|
alignContent={align.RIGHT}
|
||||||
collapseButtons={false}
|
collapseButtons={false}
|
||||||
>
|
>
|
||||||
|
{
|
||||||
|
view === 'table' ?
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
{...otherProps}
|
||||||
|
columns={columns}
|
||||||
|
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||||
|
>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Options"
|
||||||
|
iconName={icons.TABLE}
|
||||||
|
/>
|
||||||
|
</TableOptionsModalWrapper> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
view === 'posters' &&
|
view === 'posters' ?
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Options"
|
label="Options"
|
||||||
iconName={icons.POSTER}
|
iconName={icons.POSTER}
|
||||||
isDisabled={hasNoArtist}
|
isDisabled={hasNoArtist}
|
||||||
onPress={this.onPosterOptionsPress}
|
onPress={this.onPosterOptionsPress}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
view === 'banners' &&
|
view === 'banners' ?
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Options"
|
label="Options"
|
||||||
iconName={icons.POSTER}
|
iconName={icons.POSTER}
|
||||||
isDisabled={hasNoArtist}
|
isDisabled={hasNoArtist}
|
||||||
onPress={this.onBannerOptionsPress}
|
onPress={this.onBannerOptionsPress}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
view === 'overview' &&
|
view === 'overview' ?
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Options"
|
label="Options"
|
||||||
iconName={icons.OVERVIEW}
|
iconName={icons.OVERVIEW}
|
||||||
isDisabled={hasNoArtist}
|
isDisabled={hasNoArtist}
|
||||||
onPress={this.onOverviewOptionsPress}
|
onPress={this.onOverviewOptionsPress}
|
||||||
/>
|
/> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -382,6 +402,7 @@ ArtistIndex.propTypes = {
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
totalItems: PropTypes.number.isRequired,
|
totalItems: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
customFilters: 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 createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
import { fetchArtist } from 'Store/Actions/artistActions';
|
import { fetchArtist } from 'Store/Actions/artistActions';
|
||||||
import scrollPositions from 'Store/scrollPositions';
|
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 { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as commandNames from 'Commands/commandNames';
|
import * as commandNames from 'Commands/commandNames';
|
||||||
import withScrollPosition from 'Components/withScrollPosition';
|
import withScrollPosition from 'Components/withScrollPosition';
|
||||||
|
@ -66,13 +66,41 @@ function createMapStateToProps() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
fetchArtist,
|
return {
|
||||||
setArtistSort,
|
dispatchFetchArtist() {
|
||||||
setArtistFilter,
|
dispatch(fetchArtist);
|
||||||
setArtistView,
|
},
|
||||||
executeCommand
|
|
||||||
};
|
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 {
|
class ArtistIndexConnector extends Component {
|
||||||
|
|
||||||
|
@ -94,24 +122,16 @@ class ArtistIndexConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.fetchArtist();
|
this.props.dispatchFetchArtist();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
onSortSelect = (sortKey) => {
|
|
||||||
this.props.setArtistSort({ sortKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterSelect = (selectedFilterKey) => {
|
|
||||||
this.props.setArtistFilter({ selectedFilterKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewSelect = (view) => {
|
onViewSelect = (view) => {
|
||||||
// Reset the scroll position before changing the view
|
// Reset the scroll position before changing the view
|
||||||
this.setState({ scrollTop: 0 }, () => {
|
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
|
// Render
|
||||||
|
|
||||||
|
@ -143,12 +151,8 @@ class ArtistIndexConnector extends Component {
|
||||||
<ArtistIndex
|
<ArtistIndex
|
||||||
{...this.props}
|
{...this.props}
|
||||||
scrollTop={this.state.scrollTop}
|
scrollTop={this.state.scrollTop}
|
||||||
onSortSelect={this.onSortSelect}
|
|
||||||
onFilterSelect={this.onFilterSelect}
|
|
||||||
onViewSelect={this.onViewSelect}
|
onViewSelect={this.onViewSelect}
|
||||||
onScroll={this.onScroll}
|
onScroll={this.onScroll}
|
||||||
onRefreshArtistPress={this.onRefreshArtistPress}
|
|
||||||
onRssSyncPress={this.onRssSyncPress}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -158,14 +162,10 @@ ArtistIndexConnector.propTypes = {
|
||||||
isSmallScreen: PropTypes.bool.isRequired,
|
isSmallScreen: PropTypes.bool.isRequired,
|
||||||
view: PropTypes.string.isRequired,
|
view: PropTypes.string.isRequired,
|
||||||
scrollTop: PropTypes.number.isRequired,
|
scrollTop: PropTypes.number.isRequired,
|
||||||
fetchArtist: PropTypes.func.isRequired,
|
dispatchFetchArtist: PropTypes.func.isRequired
|
||||||
setArtistSort: PropTypes.func.isRequired,
|
|
||||||
setArtistFilter: PropTypes.func.isRequired,
|
|
||||||
setArtistView: PropTypes.func.isRequired,
|
|
||||||
executeCommand: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withScrollPosition(
|
export default withScrollPosition(
|
||||||
connect(createMapStateToProps, mapDispatchToProps)(ArtistIndexConnector),
|
connect(createMapStateToProps, createMapDispatchToProps)(ArtistIndexConnector),
|
||||||
'artistIndex'
|
'artistIndex'
|
||||||
);
|
);
|
||||||
|
|
|
@ -34,12 +34,20 @@
|
||||||
composes: legendItemColor;
|
composes: legendItemColor;
|
||||||
|
|
||||||
background-color: $dangerColor;
|
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 {
|
.missingUnmonitored {
|
||||||
composes: legendItemColor;
|
composes: legendItemColor;
|
||||||
|
|
||||||
background-color: $warningColor;
|
background-color: $warningColor;
|
||||||
|
|
||||||
|
&:global(.colorImpaired) {
|
||||||
|
background: repeating-linear-gradient(45deg, $warningColor, $warningColor 5px, color($warningColor tint(15%)) 5px, color($warningColor tint(15%)) 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.statistics {
|
.statistics {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import { ColorImpairedConsumer } from 'App/ColorImpairedContext';
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
import styles from './ArtistIndexFooter.css';
|
import styles from './ArtistIndexFooter.css';
|
||||||
|
@ -39,26 +41,49 @@ function ArtistIndexFooter({ artist }) {
|
||||||
totalFileSize += sizeOnDisk;
|
totalFileSize += sizeOnDisk;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorImpairedConsumer>
|
||||||
|
{(enableColorImpairedMode) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.footer}>
|
<div className={styles.footer}>
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.legendItem}>
|
<div className={styles.legendItem}>
|
||||||
<div className={styles.continuing} />
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.continuing,
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div>Continuing (All tracks downloaded)</div>
|
<div>Continuing (All tracks downloaded)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
<div className={styles.legendItem}>
|
||||||
<div className={styles.ended} />
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.ended,
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div>Ended (All tracks downloaded)</div>
|
<div>Ended (All tracks downloaded)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
<div className={styles.legendItem}>
|
||||||
<div className={styles.missingMonitored} />
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.missingMonitored,
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div>Missing Tracks (Artist monitored)</div>
|
<div>Missing Tracks (Artist monitored)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.legendItem}>
|
<div className={styles.legendItem}>
|
||||||
<div className={styles.missingUnmonitored} />
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.missingUnmonitored,
|
||||||
|
enableColorImpairedMode && 'colorImpaired'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<div>Missing Tracks (Artist not monitored)</div>
|
<div>Missing Tracks (Artist not monitored)</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,6 +139,9 @@ function ArtistIndexFooter({ artist }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}}
|
||||||
|
</ColorImpairedConsumer>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistIndexFooter.propTypes = {
|
ArtistIndexFooter.propTypes = {
|
||||||
|
|
|
@ -26,10 +26,27 @@ $hoverScale: 1.05;
|
||||||
.link {
|
.link {
|
||||||
composes: link from 'Components/Link/Link.css';
|
composes: link from 'Components/Link/Link.css';
|
||||||
|
|
||||||
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
height: 70px;
|
||||||
background-color: $defaultColor;
|
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 {
|
.nextAiring {
|
||||||
background-color: #fafbfc;
|
background-color: #fafbfc;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -49,6 +66,7 @@ $hoverScale: 1.05;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
z-index: 1;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-width: 0 25px 25px 0;
|
border-width: 0 25px 25px 0;
|
||||||
|
|
|
@ -22,6 +22,7 @@ class ArtistIndexPoster extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
hasPosterError: false,
|
||||||
isEditArtistModalOpen: false,
|
isEditArtistModalOpen: false,
|
||||||
isDeleteArtistModalOpen: false
|
isDeleteArtistModalOpen: false
|
||||||
};
|
};
|
||||||
|
@ -49,6 +50,18 @@ class ArtistIndexPoster extends Component {
|
||||||
this.setState({ isDeleteArtistModalOpen: false });
|
this.setState({ isDeleteArtistModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPosterLoad = () => {
|
||||||
|
if (this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onPosterLoadError = () => {
|
||||||
|
if (!this.state.hasPosterError) {
|
||||||
|
this.setState({ hasPosterError: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -90,6 +103,7 @@ class ArtistIndexPoster extends Component {
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
hasPosterError,
|
||||||
isEditArtistModalOpen,
|
isEditArtistModalOpen,
|
||||||
isDeleteArtistModalOpen
|
isDeleteArtistModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
@ -153,7 +167,17 @@ class ArtistIndexPoster extends Component {
|
||||||
size={250}
|
size={250}
|
||||||
lazy={false}
|
lazy={false}
|
||||||
overflow={true}
|
overflow={true}
|
||||||
|
onError={this.onPosterLoadError}
|
||||||
|
onLoad={this.onPosterLoad}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{
|
||||||
|
hasPosterError &&
|
||||||
|
<div className={styles.overlayTitle}>
|
||||||
|
{artistName}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,20 @@
|
||||||
flex: 4 0 110px;
|
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,
|
.qualityProfileId,
|
||||||
.languageProfileId,
|
.languageProfileId,
|
||||||
.metadataProfileId {
|
.metadataProfileId {
|
||||||
|
@ -40,7 +54,6 @@
|
||||||
flex: 0 0 150px;
|
flex: 0 0 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artistType,
|
|
||||||
.trackCount {
|
.trackCount {
|
||||||
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
composes: headerCell from 'Components/Table/VirtualTableHeaderCell.css';
|
||||||
|
|
||||||
|
|
|
@ -1,47 +1,22 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
import VirtualTableHeader from 'Components/Table/VirtualTableHeader';
|
||||||
import VirtualTableHeaderCell from 'Components/Table/VirtualTableHeaderCell';
|
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 ArtistIndexTableOptionsConnector from './ArtistIndexTableOptionsConnector';
|
||||||
import styles from './ArtistIndexHeader.css';
|
import styles from './ArtistIndexHeader.css';
|
||||||
|
|
||||||
class ArtistIndexHeader extends Component {
|
function ArtistIndexHeader(props) {
|
||||||
|
|
||||||
//
|
|
||||||
// Lifecycle
|
|
||||||
|
|
||||||
constructor(props, context) {
|
|
||||||
super(props, context);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
isTableOptionsModalOpen: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onTableOptionsPress = () => {
|
|
||||||
this.setState({ isTableOptionsModalOpen: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
onTableOptionsModalClose = () => {
|
|
||||||
this.setState({ isTableOptionsModalOpen: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Render
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
const {
|
||||||
showSearchAction,
|
showBanners,
|
||||||
columns,
|
columns,
|
||||||
onTableOptionChange,
|
onTableOptionChange,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VirtualTableHeader>
|
<VirtualTableHeader>
|
||||||
|
@ -67,10 +42,16 @@ class ArtistIndexHeader extends Component {
|
||||||
isSortable={false}
|
isSortable={false}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
<TableOptionsModalWrapper
|
||||||
|
columns={columns}
|
||||||
|
optionsComponent={ArtistIndexTableOptionsConnector}
|
||||||
|
onTableOptionChange={onTableOptionChange}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
name={icons.ADVANCED_SETTINGS}
|
name={icons.ADVANCED_SETTINGS}
|
||||||
onPress={this.onTableOptionsPress}
|
|
||||||
/>
|
/>
|
||||||
|
</TableOptionsModalWrapper>
|
||||||
</VirtualTableHeaderCell>
|
</VirtualTableHeaderCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -78,7 +59,11 @@ class ArtistIndexHeader extends Component {
|
||||||
return (
|
return (
|
||||||
<VirtualTableHeaderCell
|
<VirtualTableHeaderCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={classNames(
|
||||||
|
styles[name],
|
||||||
|
name === 'sortName' && showBanners && styles.banner,
|
||||||
|
name === 'sortName' && showBanners && !hasGrowableColumns(columns) && styles.bannerGrow
|
||||||
|
)}
|
||||||
name={name}
|
name={name}
|
||||||
isSortable={isSortable}
|
isSortable={isSortable}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
@ -88,22 +73,14 @@ class ArtistIndexHeader extends Component {
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
<TableOptionsModal
|
|
||||||
isOpen={this.state.isTableOptionsModalOpen}
|
|
||||||
columns={columns}
|
|
||||||
optionsComponent={ArtistIndexTableOptionsConnector}
|
|
||||||
onTableOptionChange={onTableOptionChange}
|
|
||||||
onModalClose={this.onTableOptionsModalClose}
|
|
||||||
/>
|
|
||||||
</VirtualTableHeader>
|
</VirtualTableHeader>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistIndexHeader.propTypes = {
|
ArtistIndexHeader.propTypes = {
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired
|
onTableOptionChange: PropTypes.func.isRequired,
|
||||||
|
showBanners: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ArtistIndexHeader;
|
export default ArtistIndexHeader;
|
||||||
|
|
|
@ -1,19 +1,69 @@
|
||||||
.status {
|
.cell {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 60px;
|
flex: 0 0 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sortName {
|
.sortName {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 4 0 110px;
|
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,
|
.qualityProfileId,
|
||||||
.languageProfileId,
|
.languageProfileId,
|
||||||
.metadataProfileId {
|
.metadataProfileId {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 1 0 125px;
|
flex: 1 0 125px;
|
||||||
}
|
}
|
||||||
|
@ -22,19 +72,19 @@
|
||||||
.lastAlbum,
|
.lastAlbum,
|
||||||
.added,
|
.added,
|
||||||
.genres {
|
.genres {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 180px;
|
flex: 0 0 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.albumCount {
|
.albumCount {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 100px;
|
flex: 0 0 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trackProgress {
|
.trackProgress {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -42,21 +92,20 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artistType,
|
|
||||||
.trackCount {
|
.trackCount {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 130px;
|
flex: 0 0 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path {
|
.path {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 1 0 150px;
|
flex: 1 0 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sizeOnDisk {
|
.sizeOnDisk {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 120px;
|
flex: 0 0 120px;
|
||||||
}
|
}
|
||||||
|
@ -68,21 +117,21 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags {
|
.tags {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 1 0 60px;
|
flex: 1 0 60px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.useSceneNumbering {
|
.useSceneNumbering {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 0 145px;
|
flex: 0 0 145px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.actions {
|
.actions {
|
||||||
composes: cell from 'Components/Table/Cells/VirtualTableRowCell.css';
|
composes: cell;
|
||||||
|
|
||||||
flex: 0 1 90px;
|
flex: 0 0 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkInput {
|
.checkInput {
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
|
import getProgressBarKind from 'Utilities/Artist/getProgressBarKind';
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import HeartRating from 'Components/HeartRating';
|
import HeartRating from 'Components/HeartRating';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||||
import ProgressBar from 'Components/ProgressBar';
|
import ProgressBar from 'Components/ProgressBar';
|
||||||
import TagListConnector from 'Components/TagListConnector';
|
import TagListConnector from 'Components/TagListConnector';
|
||||||
|
@ -16,6 +18,8 @@ import ArtistNameLink from 'Artist/ArtistNameLink';
|
||||||
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
import AlbumTitleLink from 'Album/AlbumTitleLink';
|
||||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||||
|
import ArtistBanner from 'Artist/ArtistBanner';
|
||||||
|
import hasGrowableColumns from './hasGrowableColumns';
|
||||||
import ArtistStatusCell from './ArtistStatusCell';
|
import ArtistStatusCell from './ArtistStatusCell';
|
||||||
import styles from './ArtistIndexRow.css';
|
import styles from './ArtistIndexRow.css';
|
||||||
|
|
||||||
|
@ -28,6 +32,7 @@ class ArtistIndexRow extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
hasBannerError: false,
|
||||||
isEditArtistModalOpen: false,
|
isEditArtistModalOpen: false,
|
||||||
isDeleteArtistModalOpen: 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
|
// Render
|
||||||
|
|
||||||
|
@ -80,6 +97,8 @@ class ArtistIndexRow extends Component {
|
||||||
ratings,
|
ratings,
|
||||||
path,
|
path,
|
||||||
tags,
|
tags,
|
||||||
|
images,
|
||||||
|
showBanners,
|
||||||
showSearchAction,
|
showSearchAction,
|
||||||
columns,
|
columns,
|
||||||
isRefreshingArtist,
|
isRefreshingArtist,
|
||||||
|
@ -97,6 +116,7 @@ class ArtistIndexRow extends Component {
|
||||||
} = statistics;
|
} = statistics;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
hasBannerError,
|
||||||
isEditArtistModalOpen,
|
isEditArtistModalOpen,
|
||||||
isDeleteArtistModalOpen
|
isDeleteArtistModalOpen
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
@ -130,12 +150,40 @@ class ArtistIndexRow extends Component {
|
||||||
return (
|
return (
|
||||||
<VirtualTableRowCell
|
<VirtualTableRowCell
|
||||||
key={name}
|
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
|
<ArtistNameLink
|
||||||
foreignArtistId={foreignArtistId}
|
foreignArtistId={foreignArtistId}
|
||||||
artistName={artistName}
|
artistName={artistName}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
</VirtualTableRowCell>
|
</VirtualTableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -424,6 +472,8 @@ ArtistIndexRow.propTypes = {
|
||||||
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
genres: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
ratings: PropTypes.object.isRequired,
|
ratings: PropTypes.object.isRequired,
|
||||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
showBanners: PropTypes.bool.isRequired,
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
showSearchAction: PropTypes.bool.isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isRefreshingArtist: PropTypes.bool.isRequired,
|
isRefreshingArtist: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -43,7 +43,8 @@ class ArtistIndexTable extends Component {
|
||||||
rowRenderer = ({ key, rowIndex, style }) => {
|
rowRenderer = ({ key, rowIndex, style }) => {
|
||||||
const {
|
const {
|
||||||
items,
|
items,
|
||||||
columns
|
columns,
|
||||||
|
showBanners
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const artist = items[rowIndex];
|
const artist = items[rowIndex];
|
||||||
|
@ -58,6 +59,7 @@ class ArtistIndexTable extends Component {
|
||||||
languageProfileId={artist.languageProfileId}
|
languageProfileId={artist.languageProfileId}
|
||||||
qualityProfileId={artist.qualityProfileId}
|
qualityProfileId={artist.qualityProfileId}
|
||||||
metadataProfileId={artist.metadataProfileId}
|
metadataProfileId={artist.metadataProfileId}
|
||||||
|
showBanners={showBanners}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -72,6 +74,7 @@ class ArtistIndexTable extends Component {
|
||||||
filters,
|
filters,
|
||||||
sortKey,
|
sortKey,
|
||||||
sortDirection,
|
sortDirection,
|
||||||
|
showBanners,
|
||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
scrollTop,
|
scrollTop,
|
||||||
contentBody,
|
contentBody,
|
||||||
|
@ -88,11 +91,12 @@ class ArtistIndexTable extends Component {
|
||||||
scrollIndex={this.state.scrollIndex}
|
scrollIndex={this.state.scrollIndex}
|
||||||
contentBody={contentBody}
|
contentBody={contentBody}
|
||||||
isSmallScreen={isSmallScreen}
|
isSmallScreen={isSmallScreen}
|
||||||
rowHeight={38}
|
rowHeight={showBanners ? 70 : 38}
|
||||||
overscanRowCount={2}
|
overscanRowCount={2}
|
||||||
rowRenderer={this.rowRenderer}
|
rowRenderer={this.rowRenderer}
|
||||||
header={
|
header={
|
||||||
<ArtistIndexHeaderConnector
|
<ArtistIndexHeaderConnector
|
||||||
|
showBanners={showBanners}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
sortKey={sortKey}
|
sortKey={sortKey}
|
||||||
sortDirection={sortDirection}
|
sortDirection={sortDirection}
|
||||||
|
@ -116,6 +120,7 @@ ArtistIndexTable.propTypes = {
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
sortKey: PropTypes.string,
|
sortKey: PropTypes.string,
|
||||||
sortDirection: PropTypes.oneOf(sortDirections.all),
|
sortDirection: PropTypes.oneOf(sortDirections.all),
|
||||||
|
showBanners: PropTypes.bool.isRequired,
|
||||||
scrollTop: PropTypes.number.isRequired,
|
scrollTop: PropTypes.number.isRequired,
|
||||||
jumpToCharacter: PropTypes.string,
|
jumpToCharacter: PropTypes.string,
|
||||||
contentBody: PropTypes.object.isRequired,
|
contentBody: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -11,7 +11,8 @@ function createMapStateToProps() {
|
||||||
(dimensions, artist) => {
|
(dimensions, artist) => {
|
||||||
return {
|
return {
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
...artist
|
...artist,
|
||||||
|
showBanners: artist.tableOptions.showBanners
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component, Fragment } from 'react';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
|
@ -14,15 +14,23 @@ class ArtistIndexTableOptions extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
|
showBanners: props.showBanners,
|
||||||
showSearchAction: props.showSearchAction
|
showSearchAction: props.showSearchAction
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const { showSearchAction } = this.props;
|
const {
|
||||||
|
showBanners,
|
||||||
|
showSearchAction
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if (showSearchAction !== prevProps.showSearchAction) {
|
if (
|
||||||
|
showBanners !== prevProps.showBanners ||
|
||||||
|
showSearchAction !== prevProps.showSearchAction
|
||||||
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
showBanners,
|
||||||
showSearchAction
|
showSearchAction
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -49,10 +57,24 @@ class ArtistIndexTableOptions extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
showBanners,
|
||||||
showSearchAction
|
showSearchAction
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
return (
|
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>
|
<FormGroup>
|
||||||
<FormLabel>Show Search</FormLabel>
|
<FormLabel>Show Search</FormLabel>
|
||||||
|
|
||||||
|
@ -60,15 +82,17 @@ class ArtistIndexTableOptions extends Component {
|
||||||
type={inputTypes.CHECK}
|
type={inputTypes.CHECK}
|
||||||
name="showSearchAction"
|
name="showSearchAction"
|
||||||
value={showSearchAction}
|
value={showSearchAction}
|
||||||
helpText="Show search button"
|
helpText="Show search button on hover"
|
||||||
onChange={this.onTableOptionChange}
|
onChange={this.onTableOptionChange}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
</Fragment>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistIndexTableOptions.propTypes = {
|
ArtistIndexTableOptions.propTypes = {
|
||||||
|
showBanners: PropTypes.bool.isRequired,
|
||||||
showSearchAction: PropTypes.bool.isRequired,
|
showSearchAction: PropTypes.bool.isRequired,
|
||||||
onTableOptionChange: PropTypes.func.isRequired
|
onTableOptionChange: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
17
frontend/src/Artist/Index/Table/hasGrowableColumns.js
Normal file
17
frontend/src/Artist/Index/Table/hasGrowableColumns.js
Normal file
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
33
frontend/src/Artist/Search/ArtistInteractiveSearchModal.js
Normal file
33
frontend/src/Artist/Search/ArtistInteractiveSearchModal.js
Normal file
|
@ -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;
|
overflow-x: hidden;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border-bottom: 1px solid $borderColor;
|
border-bottom: 1px solid $borderColor;
|
||||||
font-size: 14px;
|
font-size: $defaultFontSize;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $tableRowHoverBackgroundColor;
|
background-color: $tableRowHoverBackgroundColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.eventWrapper {
|
||||||
width: 10px;
|
display: flex;
|
||||||
|
flex: 1 0 1px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding-left: 6px;
|
||||||
border-left-width: 4px;
|
border-left-width: 4px;
|
||||||
border-left-style: solid;
|
border-left-style: solid;
|
||||||
}
|
}
|
||||||
|
@ -24,6 +27,7 @@
|
||||||
.time {
|
.time {
|
||||||
flex: 0 0 120px;
|
flex: 0 0 120px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.artistName,
|
.artistName,
|
||||||
|
@ -80,16 +84,16 @@
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.event {
|
.event {
|
||||||
position: relative;
|
flex-direction: column;
|
||||||
flex-wrap: wrap;
|
|
||||||
padding-left: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.eventWrapper {
|
||||||
position: absolute;
|
display: block;
|
||||||
top: 7%;
|
flex: 0 0 auto;
|
||||||
left: 0;
|
}
|
||||||
height: 86%;
|
|
||||||
|
.date {
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date,
|
.date,
|
||||||
|
|
|
@ -49,7 +49,8 @@ class AgendaEvent extends Component {
|
||||||
queueItem,
|
queueItem,
|
||||||
showDate,
|
showDate,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
longDateFormat
|
longDateFormat,
|
||||||
|
colorImpairedMode
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const startTime = moment(releaseDate);
|
const startTime = moment(releaseDate);
|
||||||
|
@ -74,8 +75,9 @@ class AgendaEvent extends Component {
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.status,
|
styles.eventWrapper,
|
||||||
styles[statusStyle]
|
styles[statusStyle],
|
||||||
|
colorImpairedMode && 'colorImpaired'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,8 @@ function createMapStateToProps() {
|
||||||
artist,
|
artist,
|
||||||
queueItem,
|
queueItem,
|
||||||
timeFormat: uiSettings.timeFormat,
|
timeFormat: uiSettings.timeFormat,
|
||||||
longDateFormat: uiSettings.longDateFormat
|
longDateFormat: uiSettings.longDateFormat,
|
||||||
|
colorImpairedMode: uiSettings.enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -41,8 +41,20 @@ class CalendarConnector extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
const {
|
||||||
|
useCurrentPage,
|
||||||
|
fetchCalendar,
|
||||||
|
gotoCalendarToday
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
registerPagePopulator(this.repopulate);
|
registerPagePopulator(this.repopulate);
|
||||||
this.props.gotoCalendarToday();
|
|
||||||
|
if (useCurrentPage) {
|
||||||
|
fetchCalendar();
|
||||||
|
} else {
|
||||||
|
gotoCalendarToday();
|
||||||
|
}
|
||||||
|
|
||||||
this.scheduleUpdate();
|
this.scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import NoArtist from 'Artist/NoArtist';
|
import NoArtist from 'Artist/NoArtist';
|
||||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
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 CalendarConnector from './CalendarConnector';
|
||||||
import styles from './CalendarPage.css';
|
import styles from './CalendarPage.css';
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@ class CalendarPage extends Component {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isCalendarLinkModalOpen: false,
|
isCalendarLinkModalOpen: false,
|
||||||
|
isOptionsModalOpen: false,
|
||||||
width: 0
|
width: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -48,6 +50,23 @@ class CalendarPage extends Component {
|
||||||
this.setState({ isCalendarLinkModalOpen: false });
|
this.setState({ isCalendarLinkModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOptionsPress = () => {
|
||||||
|
this.setState({ isOptionsModalOpen: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onOptionsModalClose = () => {
|
||||||
|
this.setState({ isOptionsModalOpen: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearchMissingPress = () => {
|
||||||
|
const {
|
||||||
|
missingAlbumIds,
|
||||||
|
onSearchMissingPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onSearchMissingPress(missingAlbumIds);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -56,17 +75,20 @@ class CalendarPage extends Component {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
hasArtist,
|
hasArtist,
|
||||||
colorImpairedMode,
|
missingAlbumIds,
|
||||||
|
isSearchingForMissing,
|
||||||
|
useCurrentPage,
|
||||||
onFilterSelect
|
onFilterSelect
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isCalendarLinkModalOpen,
|
||||||
|
isOptionsModalOpen
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
const isMeasured = this.state.width > 0;
|
const isMeasured = this.state.width > 0;
|
||||||
|
|
||||||
let PageComponent = 'div';
|
const PageComponent = hasArtist ? CalendarConnector : NoArtist;
|
||||||
|
|
||||||
if (isMeasured) {
|
|
||||||
PageComponent = hasArtist ? CalendarConnector : NoArtist;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent title="Calendar">
|
<PageContent title="Calendar">
|
||||||
|
@ -77,9 +99,23 @@ class CalendarPage extends Component {
|
||||||
iconName={icons.CALENDAR}
|
iconName={icons.CALENDAR}
|
||||||
onPress={this.onGetCalendarLinkPress}
|
onPress={this.onGetCalendarLinkPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Search for Missing"
|
||||||
|
iconName={icons.SEARCH}
|
||||||
|
isDisabled={!missingAlbumIds.length}
|
||||||
|
isSpinning={isSearchingForMissing}
|
||||||
|
onPress={this.onSearchMissingPress}
|
||||||
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
|
|
||||||
<PageToolbarSection alignContent={align.RIGHT}>
|
<PageToolbarSection alignContent={align.RIGHT}>
|
||||||
|
<PageToolbarButton
|
||||||
|
label="Options"
|
||||||
|
iconName={icons.POSTER}
|
||||||
|
onPress={this.onOptionsPress}
|
||||||
|
/>
|
||||||
|
|
||||||
<FilterMenu
|
<FilterMenu
|
||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
isDisabled={!hasArtist}
|
isDisabled={!hasArtist}
|
||||||
|
@ -99,19 +135,31 @@ class CalendarPage extends Component {
|
||||||
whitelist={['width']}
|
whitelist={['width']}
|
||||||
onMeasure={this.onMeasure}
|
onMeasure={this.onMeasure}
|
||||||
>
|
>
|
||||||
<PageComponent />
|
{
|
||||||
|
isMeasured ?
|
||||||
|
<PageComponent
|
||||||
|
useCurrentPage={useCurrentPage}
|
||||||
|
/> :
|
||||||
|
<div />
|
||||||
|
}
|
||||||
</Measure>
|
</Measure>
|
||||||
|
|
||||||
{
|
{
|
||||||
hasArtist &&
|
hasArtist &&
|
||||||
<Legend colorImpairedMode={colorImpairedMode} />
|
<LegendConnector />
|
||||||
}
|
}
|
||||||
</PageContentBodyConnector>
|
</PageContentBodyConnector>
|
||||||
|
|
||||||
<CalendarLinkModal
|
<CalendarLinkModal
|
||||||
isOpen={this.state.isCalendarLinkModalOpen}
|
isOpen={isCalendarLinkModalOpen}
|
||||||
onModalClose={this.onGetCalendarLinkModalClose}
|
onModalClose={this.onGetCalendarLinkModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<CalendarOptionsModal
|
||||||
|
isOpen={isOptionsModalOpen}
|
||||||
|
onModalClose={this.onOptionsModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -121,7 +169,10 @@ CalendarPage.propTypes = {
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.string.isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
hasArtist: PropTypes.bool.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,
|
onDaysCountChange: PropTypes.func.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired
|
onFilterSelect: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,22 +1,80 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
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 createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
|
||||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||||
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import CalendarPage from './CalendarPage';
|
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() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.calendar,
|
(state) => state.calendar.selectedFilterKey,
|
||||||
|
(state) => state.calendar.filters,
|
||||||
createArtistCountSelector(),
|
createArtistCountSelector(),
|
||||||
createUISettingsSelector(),
|
createUISettingsSelector(),
|
||||||
(calendar, artistCount, uiSettings) => {
|
createMissingAlbumIdsSelector(),
|
||||||
|
createIsSearchingSelector(),
|
||||||
|
(
|
||||||
|
selectedFilterKey,
|
||||||
|
filters,
|
||||||
|
artistCount,
|
||||||
|
uiSettings,
|
||||||
|
missingAlbumIds,
|
||||||
|
isSearchingForMissing
|
||||||
|
) => {
|
||||||
return {
|
return {
|
||||||
selectedFilterKey: calendar.selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters: calendar.filters,
|
filters,
|
||||||
showUpcoming: calendar.showUpcoming,
|
|
||||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||||
hasArtist: !!artistCount
|
hasArtist: !!artistCount,
|
||||||
|
missingAlbumIds,
|
||||||
|
isSearchingForMissing
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -24,6 +82,9 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
function createMapDispatchToProps(dispatch, props) {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
return {
|
return {
|
||||||
|
onSearchMissingPress(albumIds) {
|
||||||
|
dispatch(searchMissing({ albumIds }));
|
||||||
|
},
|
||||||
onDaysCountChange(dayCount) {
|
onDaysCountChange(dayCount) {
|
||||||
dispatch(setCalendarDaysCount({ 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 {
|
.artistName {
|
||||||
color: #3a3f51;
|
color: #3a3f51;
|
||||||
font-size: 14px;
|
font-size: $defaultFontSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.absoluteEpisodeNumber {
|
.absoluteEpisodeNumber {
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
border-left-color: $gray;
|
border-left-color: $gray;
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&: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;
|
border-left-color: $dangerColor;
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&: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;
|
border-left-color: $blue;
|
||||||
|
|
||||||
&:global(.colorImpaired) {
|
&: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 classNames from 'classnames';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import getStatusStyle from 'Calendar/getStatusStyle';
|
import getStatusStyle from 'Calendar/getStatusStyle';
|
||||||
import albumEntities from 'Album/albumEntities';
|
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
import CalendarEventQueueDetails from './CalendarEventQueueDetails';
|
||||||
|
|
|
@ -1,9 +1,29 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
import LegendItem from './LegendItem';
|
import LegendItem from './LegendItem';
|
||||||
|
import LegendIconItem from './LegendIconItem';
|
||||||
import styles from './Legend.css';
|
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 (
|
return (
|
||||||
<div className={styles.legend}>
|
<div className={styles.legend}>
|
||||||
<div>
|
<div>
|
||||||
|
@ -47,11 +67,24 @@ function Legend({ colorImpairedMode }) {
|
||||||
colorImpairedMode={colorImpairedMode}
|
colorImpairedMode={colorImpairedMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{iconsToShow[0]}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
iconsToShow.length > 1 &&
|
||||||
|
<div>
|
||||||
|
{iconsToShow[1]}
|
||||||
|
{iconsToShow[2]}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Legend.propTypes = {
|
Legend.propTypes = {
|
||||||
|
showCutoffUnmetIcon: PropTypes.bool.isRequired,
|
||||||
colorImpairedMode: PropTypes.bool.isRequired
|
colorImpairedMode: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
19
frontend/src/Calendar/Legend/LegendConnector.js
Normal file
19
frontend/src/Calendar/Legend/LegendConnector.js
Normal file
|
@ -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);
|
10
frontend/src/Calendar/Legend/LegendIconItem.css
Normal file
10
frontend/src/Calendar/Legend/LegendIconItem.css
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
.legendIconItem {
|
||||||
|
margin: 3px 0;
|
||||||
|
margin-right: 6px;
|
||||||
|
width: 150px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
37
frontend/src/Calendar/Legend/LegendIconItem.js
Normal file
37
frontend/src/Calendar/Legend/LegendIconItem.js
Normal file
|
@ -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 PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import InteractiveSearchModalContentConnector from './InteractiveSearchModalContentConnector';
|
import CalendarOptionsModalContentConnector from './CalendarOptionsModalContentConnector';
|
||||||
|
|
||||||
function InteractiveSearchModal(props) {
|
function CalendarOptionsModal(props) {
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
onModalClose,
|
onModalClose
|
||||||
...otherProps
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -15,17 +14,16 @@ function InteractiveSearchModal(props) {
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
>
|
>
|
||||||
<InteractiveSearchModalContentConnector
|
<CalendarOptionsModalContentConnector
|
||||||
{...otherProps}
|
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
InteractiveSearchModal.propTypes = {
|
CalendarOptionsModal.propTypes = {
|
||||||
isOpen: PropTypes.bool.isRequired,
|
isOpen: PropTypes.bool.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default InteractiveSearchModal;
|
export default CalendarOptionsModal;
|
216
frontend/src/Calendar/Options/CalendarOptionsModalContent.js
Normal file
216
frontend/src/Calendar/Options/CalendarOptionsModalContent.js
Normal file
|
@ -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) {
|
function getValue(input, selectedFilterBuilderProp) {
|
||||||
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
if (selectedFilterBuilderProp.valueType === filterBuilderValueTypes.BYTES) {
|
||||||
const match = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
const match = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||||
|
|
||||||
if (match && match.length > 1) {
|
if (match && match.length > 1) {
|
||||||
const [, value, unit] = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
const [, value, unit] = input.match(/^(\d+)([kmgt](i?b)?)$/i);
|
||||||
|
|
||||||
switch (unit.toLowerCase()) {
|
switch (unit.toLowerCase()) {
|
||||||
case 'k':
|
case 'k':
|
||||||
return convertToBytes(value, 1, true);
|
return convertToBytes(value, 1, true);
|
||||||
|
@ -118,6 +120,7 @@ class FilterBuilderRowValue extends Component {
|
||||||
name: tag && tag.name
|
name: tag && tag.name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: getTagDisplayValue(id, selectedFilterBuilderProp)
|
name: getTagDisplayValue(id, selectedFilterBuilderProp)
|
||||||
|
|
|
@ -12,7 +12,7 @@ function createMapStateToProps() {
|
||||||
(state) => state.settings.qualityProfiles,
|
(state) => state.settings.qualityProfiles,
|
||||||
(qualityProfiles) => {
|
(qualityProfiles) => {
|
||||||
const {
|
const {
|
||||||
isFetchingSchema: isFetching,
|
isSchemaFetching: isFetching,
|
||||||
isSchemaPopulated: isPopulated,
|
isSchemaPopulated: isPopulated,
|
||||||
schemaError: error,
|
schemaError: error,
|
||||||
schema
|
schema
|
||||||
|
|
|
@ -44,7 +44,7 @@ class AlbumReleaseSelectInputConnector extends Component {
|
||||||
albumReleases
|
albumReleases
|
||||||
} = this.props;
|
} = 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;
|
_.find(updatedReleases, { foreignReleaseId: value }).monitored = true;
|
||||||
|
|
||||||
this.props.onChange({ name, value: updatedReleases });
|
this.props.onChange({ name, value: updatedReleases });
|
||||||
|
|
58
frontend/src/Components/Form/AutoCompleteInput.css
Normal file
58
frontend/src/Components/Form/AutoCompleteInput.css
Normal file
|
@ -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;
|
||||||
|
}
|
162
frontend/src/Components/Form/AutoCompleteInput.js
Normal file
162
frontend/src/Components/Form/AutoCompleteInput.js
Normal file
|
@ -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;
|
3
frontend/src/Components/Form/Form.css
Normal file
3
frontend/src/Components/Form/Form.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.validationFailures {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
|
@ -2,11 +2,14 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { kinds } from 'Helpers/Props';
|
import { kinds } from 'Helpers/Props';
|
||||||
import Alert from 'Components/Alert';
|
import Alert from 'Components/Alert';
|
||||||
|
import styles from './Form.css';
|
||||||
|
|
||||||
function Form({ children, validationErrors, validationWarnings, ...otherProps }) {
|
function Form({ children, validationErrors, validationWarnings, ...otherProps }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
{
|
||||||
|
validationErrors.length || validationWarnings.length ?
|
||||||
|
<div className={styles.validationFailures}>
|
||||||
{
|
{
|
||||||
validationErrors.map((error, index) => {
|
validationErrors.map((error, index) => {
|
||||||
return (
|
return (
|
||||||
|
@ -32,7 +35,9 @@ function Form({ children, validationErrors, validationWarnings, ...otherProps })
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div> :
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,9 +2,11 @@ import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { inputTypes } from 'Helpers/Props';
|
import { inputTypes } from 'Helpers/Props';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
|
import AutoCompleteInput from './AutoCompleteInput';
|
||||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||||
import CheckInput from './CheckInput';
|
import CheckInput from './CheckInput';
|
||||||
import DeviceInputConnector from './DeviceInputConnector';
|
import DeviceInputConnector from './DeviceInputConnector';
|
||||||
|
import KeyValueListInput from './KeyValueListInput';
|
||||||
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
|
import MonitorAlbumsSelectInput from './MonitorAlbumsSelectInput';
|
||||||
import NumberInput from './NumberInput';
|
import NumberInput from './NumberInput';
|
||||||
import OAuthInputConnector from './OAuthInputConnector';
|
import OAuthInputConnector from './OAuthInputConnector';
|
||||||
|
@ -25,6 +27,9 @@ import styles from './FormInputGroup.css';
|
||||||
|
|
||||||
function getComponent(type) {
|
function getComponent(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case inputTypes.AUTO_COMPLETE:
|
||||||
|
return AutoCompleteInput;
|
||||||
|
|
||||||
case inputTypes.CAPTCHA:
|
case inputTypes.CAPTCHA:
|
||||||
return CaptchaInputConnector;
|
return CaptchaInputConnector;
|
||||||
|
|
||||||
|
@ -34,6 +39,9 @@ function getComponent(type) {
|
||||||
case inputTypes.DEVICE:
|
case inputTypes.DEVICE:
|
||||||
return DeviceInputConnector;
|
return DeviceInputConnector;
|
||||||
|
|
||||||
|
case inputTypes.KEY_VALUE_LIST:
|
||||||
|
return KeyValueListInput;
|
||||||
|
|
||||||
case inputTypes.MONITOR_ALBUMS_SELECT:
|
case inputTypes.MONITOR_ALBUMS_SELECT:
|
||||||
return MonitorAlbumsSelectInput;
|
return MonitorAlbumsSelectInput;
|
||||||
|
|
||||||
|
|
21
frontend/src/Components/Form/KeyValueListInput.css
Normal file
21
frontend/src/Components/Form/KeyValueListInput.css
Normal file
|
@ -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';
|
||||||
|
}
|
152
frontend/src/Components/Form/KeyValueListInput.js
Normal file
152
frontend/src/Components/Form/KeyValueListInput.js
Normal file
|
@ -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;
|
14
frontend/src/Components/Form/KeyValueListInputItem.css
Normal file
14
frontend/src/Components/Form/KeyValueListInputItem.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
.itemContainer {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
border-bottom: 1px solid $inputBorderColor;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.keyInput,
|
||||||
|
.valueInput {
|
||||||
|
border: none;
|
||||||
|
}
|
117
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
117
frontend/src/Components/Form/KeyValueListInputItem.js
Normal file
|
@ -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 PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import monitorOptions from 'Utilities/Artist/monitorOptions';
|
||||||
import SelectInput from './SelectInput';
|
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) {
|
function MonitorAlbumsSelectInput(props) {
|
||||||
const {
|
const {
|
||||||
includeNoChange,
|
includeNoChange,
|
||||||
|
|
|
@ -2,34 +2,18 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import TextInput from './TextInput';
|
import TextInput from './TextInput';
|
||||||
|
|
||||||
class NumberInput extends Component {
|
function parseValue(props, value) {
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onChange = ({ name, value }) => {
|
|
||||||
let newValue = null;
|
|
||||||
|
|
||||||
if (value) {
|
|
||||||
newValue = this.props.isFloat ? parseFloat(value) : parseInt(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.props.onChange({
|
|
||||||
name,
|
|
||||||
value: newValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onBlur = () => {
|
|
||||||
const {
|
const {
|
||||||
name,
|
isFloat,
|
||||||
value,
|
|
||||||
min,
|
min,
|
||||||
max,
|
max
|
||||||
onChange
|
} = props;
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
let newValue = value;
|
if (value == null || value === '') {
|
||||||
|
return min;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newValue = isFloat ? parseFloat(value) : parseInt(value);
|
||||||
|
|
||||||
if (min != null && newValue != null && newValue < min) {
|
if (min != null && newValue != null && newValue < min) {
|
||||||
newValue = min;
|
newValue = min;
|
||||||
|
@ -37,9 +21,72 @@ class NumberInput extends Component {
|
||||||
newValue = max;
|
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({
|
onChange({
|
||||||
name,
|
name,
|
||||||
value: newValue
|
value: parsedValue
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,18 +94,16 @@ class NumberInput extends Component {
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const value = this.state.value;
|
||||||
value,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextInput
|
<TextInput
|
||||||
|
{...this.props}
|
||||||
type="number"
|
type="number"
|
||||||
value={value == null ? '' : value}
|
value={value == null ? '' : value}
|
||||||
{...otherProps}
|
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onBlur={this.onBlur}
|
onBlur={this.onBlur}
|
||||||
|
onFocus={this.onFocus}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.freeSpace {
|
.freeSpace {
|
||||||
@add-mixin truncate;
|
flex: 0 0 auto;
|
||||||
|
|
||||||
flex: 1 0 0;
|
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
color: $gray;
|
color: $gray;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
|
@ -33,7 +33,10 @@ class TagInputTag extends Component {
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link onPress={this.onDelete}>
|
<Link
|
||||||
|
tabIndex={-1}
|
||||||
|
onPress={this.onDelete}
|
||||||
|
>
|
||||||
<Label kind={kind}>
|
<Label kind={kind}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</Label>
|
</Label>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue