mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-06 04:52:21 -07:00
Medium Support (Multi-disc Albums), Quality Grouping (#121)
* Multi Disc Stage 1 - Backend Work * Quality Group Functionality * Fixed: Only show wanted album types on ArtistDetail page * Add Media Count Column to ArtistDetail Page * Parser updates for multidisc cases, other usenet release title formats * Search for Tracks by Medium Number in Addition to Title and TrackNumber * Medium Renaming Token for Track Naming * fixup Codacy and Comment Cleanup * fixup remove comments
This commit is contained in:
parent
e1e7cad951
commit
21428cba6f
154 changed files with 2946 additions and 701 deletions
|
@ -43,7 +43,11 @@ class HistoryConnector extends Component {
|
||||||
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');
|
||||||
this.props.fetchEpisodes({ albumIds });
|
if (albumIds.length) {
|
||||||
|
this.props.fetchEpisodes({ albumIds });
|
||||||
|
} else {
|
||||||
|
this.props.clearEpisodes();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,11 @@ class QueueConnector extends Component {
|
||||||
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');
|
||||||
this.props.fetchEpisodes({ albumIds });
|
if (albumIds.length) {
|
||||||
|
this.props.fetchEpisodes({ albumIds });
|
||||||
|
} else {
|
||||||
|
this.props.clearEpisodes();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,7 +44,7 @@ class AddNewArtistSearchResult extends Component {
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (!prevProps.isExistingArtist && this.props.isExistingArtist) {
|
if (!prevProps.isExistingArtist && this.props.isExistingArtist) {
|
||||||
this.onAddSerisModalClose();
|
this.onAddArtistModalClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ class AddNewArtistSearchResult extends Component {
|
||||||
this.setState({ isNewAddArtistModalOpen: true });
|
this.setState({ isNewAddArtistModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onAddSerisModalClose = () => {
|
onAddArtistModalClose = () => {
|
||||||
this.setState({ isNewAddArtistModalOpen: false });
|
this.setState({ isNewAddArtistModalOpen: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +183,7 @@ class AddNewArtistSearchResult extends Component {
|
||||||
year={year}
|
year={year}
|
||||||
overview={overview}
|
overview={overview}
|
||||||
images={images}
|
images={images}
|
||||||
onModalClose={this.onAddSerisModalClose}
|
onModalClose={this.onAddArtistModalClose}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,13 +2,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 { clearReleases } from 'Store/Actions/releaseActions';
|
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
|
||||||
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
|
import { toggleEpisodeMonitored } from 'Store/Actions/episodeActions';
|
||||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import episodeEntities from 'Album/episodeEntities';
|
import episodeEntities from 'Album/episodeEntities';
|
||||||
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
||||||
import { fetchTrackFiles, clearTrackFiles } from 'Store/Actions/trackFileActions';
|
|
||||||
import EpisodeDetailsModalContent from './EpisodeDetailsModalContent';
|
import EpisodeDetailsModalContent from './EpisodeDetailsModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
|
@ -32,14 +31,38 @@ function createMapStateToProps() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
function createMapDispatchToProps(dispatch, props) {
|
||||||
clearReleases,
|
return {
|
||||||
fetchTracks,
|
dispatchCancelFetchReleases() {
|
||||||
clearTracks,
|
dispatch(cancelFetchReleases());
|
||||||
fetchTrackFiles,
|
},
|
||||||
clearTrackFiles,
|
|
||||||
toggleEpisodeMonitored
|
dispatchClearReleases() {
|
||||||
};
|
dispatch(clearReleases());
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchFetchTracks({ artistId, albumId }) {
|
||||||
|
dispatch(fetchTracks({ artistId, albumId }));
|
||||||
|
},
|
||||||
|
|
||||||
|
dispatchClearTracks() {
|
||||||
|
dispatch(clearTracks());
|
||||||
|
},
|
||||||
|
|
||||||
|
onMonitorAlbumPress(monitored) {
|
||||||
|
const {
|
||||||
|
albumId,
|
||||||
|
episodeEntity
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
dispatch(toggleEpisodeMonitored({
|
||||||
|
episodeEntity,
|
||||||
|
albumId,
|
||||||
|
monitored
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class EpisodeDetailsModalContentConnector extends Component {
|
class EpisodeDetailsModalContentConnector extends Component {
|
||||||
|
|
||||||
|
@ -53,7 +76,8 @@ class EpisodeDetailsModalContentConnector extends Component {
|
||||||
// Clear pending releases here so we can reshow the search
|
// Clear pending releases here so we can reshow the search
|
||||||
// results even after switching tabs.
|
// results even after switching tabs.
|
||||||
this._unpopulate();
|
this._unpopulate();
|
||||||
this.props.clearReleases();
|
this.props.dispatchCancelFetchReleases();
|
||||||
|
this.props.dispatchClearReleases();
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -62,40 +86,24 @@ class EpisodeDetailsModalContentConnector extends Component {
|
||||||
_populate() {
|
_populate() {
|
||||||
const artistId = this.props.artistId;
|
const artistId = this.props.artistId;
|
||||||
const albumId = this.props.albumId;
|
const albumId = this.props.albumId;
|
||||||
this.props.fetchTracks({ artistId, albumId });
|
this.props.dispatchFetchTracks({ artistId, albumId });
|
||||||
// this.props.fetchTrackFiles({ artistId, albumId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_unpopulate() {
|
_unpopulate() {
|
||||||
this.props.clearTracks();
|
this.props.dispatchClearTracks();
|
||||||
// this.props.clearTrackFiles();
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Listeners
|
|
||||||
|
|
||||||
onMonitorAlbumPress = (monitored) => {
|
|
||||||
const {
|
|
||||||
albumId,
|
|
||||||
episodeEntity
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.props.toggleEpisodeMonitored({
|
|
||||||
episodeEntity,
|
|
||||||
albumId,
|
|
||||||
monitored
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const {
|
||||||
|
dispatchClearReleases,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EpisodeDetailsModalContent
|
<EpisodeDetailsModalContent {...otherProps} />
|
||||||
{...this.props}
|
|
||||||
onMonitorAlbumPress={this.onMonitorAlbumPress}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,16 +112,14 @@ EpisodeDetailsModalContentConnector.propTypes = {
|
||||||
albumId: PropTypes.number.isRequired,
|
albumId: PropTypes.number.isRequired,
|
||||||
episodeEntity: PropTypes.string.isRequired,
|
episodeEntity: PropTypes.string.isRequired,
|
||||||
artistId: PropTypes.number.isRequired,
|
artistId: PropTypes.number.isRequired,
|
||||||
fetchTracks: PropTypes.func.isRequired,
|
dispatchFetchTracks: PropTypes.func.isRequired,
|
||||||
clearTracks: PropTypes.func.isRequired,
|
dispatchClearTracks: PropTypes.func.isRequired,
|
||||||
fetchTrackFiles: PropTypes.func.isRequired,
|
dispatchCancelFetchReleases: PropTypes.func.isRequired,
|
||||||
clearTrackFiles: PropTypes.func.isRequired,
|
dispatchClearReleases: PropTypes.func.isRequired
|
||||||
clearReleases: PropTypes.func.isRequired,
|
|
||||||
toggleEpisodeMonitored: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeDetailsModalContentConnector.defaultProps = {
|
EpisodeDetailsModalContentConnector.defaultProps = {
|
||||||
episodeEntity: episodeEntities.EPISODES
|
episodeEntity: episodeEntities.EPISODES
|
||||||
};
|
};
|
||||||
|
|
||||||
export default connect(createMapStateToProps, mapDispatchToProps)(EpisodeDetailsModalContentConnector);
|
export default connect(createMapStateToProps, createMapDispatchToProps)(EpisodeDetailsModalContentConnector);
|
||||||
|
|
|
@ -3,20 +3,24 @@ import React from 'react';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
|
|
||||||
function EpisodeLanguage(props) {
|
function EpisodeLanguage(props) {
|
||||||
const language = props.language;
|
const {
|
||||||
|
className,
|
||||||
|
language
|
||||||
|
} = props;
|
||||||
|
|
||||||
if (!language) {
|
if (!language) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label>
|
<Label className={className}>
|
||||||
{language.name}
|
{language.name}
|
||||||
</Label>
|
</Label>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
EpisodeLanguage.propTypes = {
|
EpisodeLanguage.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
language: PropTypes.object
|
language: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ function getTooltip(title, quality, size) {
|
||||||
|
|
||||||
function EpisodeQuality(props) {
|
function EpisodeQuality(props) {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
title,
|
title,
|
||||||
quality,
|
quality,
|
||||||
size,
|
size,
|
||||||
|
@ -32,6 +33,7 @@ function EpisodeQuality(props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Label
|
||||||
|
className={className}
|
||||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||||
title={getTooltip(title, quality, size)}
|
title={getTooltip(title, quality, size)}
|
||||||
>
|
>
|
||||||
|
@ -41,6 +43,7 @@ function EpisodeQuality(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
EpisodeQuality.propTypes = {
|
EpisodeQuality.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
size: PropTypes.number,
|
size: PropTypes.number,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import titleCase from 'Utilities/String/titleCase';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
@ -14,6 +13,18 @@ import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConn
|
||||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||||
import styles from './AlbumHistoryRow.css';
|
import styles from './AlbumHistoryRow.css';
|
||||||
|
|
||||||
|
function getTitle(eventType) {
|
||||||
|
switch (eventType) {
|
||||||
|
case 'grabbed': return 'Grabbed';
|
||||||
|
case 'artistFolderImported': return 'Artist Folder Imported';
|
||||||
|
case 'downloadFolderImported': return 'Download Folder Imported';
|
||||||
|
case 'downloadFailed': return 'Download Failed';
|
||||||
|
case 'trackFileDeleted': return 'Track File Deleted';
|
||||||
|
case 'trackFileRenamed': return 'Track File Renamed';
|
||||||
|
default: return 'Unknown';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class AlbumHistoryRow extends Component {
|
class AlbumHistoryRow extends Component {
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -89,7 +100,7 @@ class AlbumHistoryRow extends Component {
|
||||||
name={icons.INFO}
|
name={icons.INFO}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
title={titleCase(eventType)}
|
title={getTitle(eventType)}
|
||||||
body={
|
body={
|
||||||
<HistoryDetailsConnector
|
<HistoryDetailsConnector
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
|
|
@ -15,7 +15,9 @@ function createMapStateToProps() {
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
(tracks, episode, commands, dimensions) => {
|
(tracks, episode, commands, dimensions) => {
|
||||||
const items = _.filter(tracks.items, { albumId: episode.id });
|
const filteredItems = _.filter(tracks.items, { albumId: episode.id });
|
||||||
|
const mediumSortedItems = _.orderBy(filteredItems, 'absoluteTrackNumber');
|
||||||
|
const items = _.orderBy(mediumSortedItems, 'mediumNumber');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
network: episode.label,
|
network: episode.label,
|
||||||
|
|
|
@ -24,7 +24,8 @@ class TrackDetailRow extends Component {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
title,
|
title,
|
||||||
trackNumber,
|
mediumNumber,
|
||||||
|
absoluteTrackNumber,
|
||||||
duration,
|
duration,
|
||||||
columns,
|
columns,
|
||||||
trackFileId
|
trackFileId
|
||||||
|
@ -43,13 +44,24 @@ class TrackDetailRow extends Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (name === 'trackNumber') {
|
if (name === 'medium') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell
|
<TableRowCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles.trackNumber}
|
className={styles.trackNumber}
|
||||||
>
|
>
|
||||||
{trackNumber}
|
{mediumNumber}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === 'absoluteTrackNumber') {
|
||||||
|
return (
|
||||||
|
<TableRowCell
|
||||||
|
key={name}
|
||||||
|
className={styles.trackNumber}
|
||||||
|
>
|
||||||
|
{absoluteTrackNumber}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -117,7 +129,8 @@ TrackDetailRow.propTypes = {
|
||||||
duration: PropTypes.number.isRequired,
|
duration: PropTypes.number.isRequired,
|
||||||
trackFileId: PropTypes.number.isRequired,
|
trackFileId: PropTypes.number.isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
trackNumber: PropTypes.number.isRequired
|
mediumNumber: PropTypes.number.isRequired,
|
||||||
|
absoluteTrackNumber: PropTypes.number.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TrackDetailRow;
|
export default TrackDetailRow;
|
||||||
|
|
|
@ -63,6 +63,7 @@ class AlbumRow extends Component {
|
||||||
statistics,
|
statistics,
|
||||||
duration,
|
duration,
|
||||||
releaseDate,
|
releaseDate,
|
||||||
|
mediumCount,
|
||||||
title,
|
title,
|
||||||
isSaving,
|
isSaving,
|
||||||
artistMonitored,
|
artistMonitored,
|
||||||
|
@ -131,6 +132,16 @@ class AlbumRow extends Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (name === 'mediumCount') {
|
||||||
|
return (
|
||||||
|
<TableRowCell key={name}>
|
||||||
|
{
|
||||||
|
mediumCount
|
||||||
|
}
|
||||||
|
</TableRowCell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (name === 'trackCount') {
|
if (name === 'trackCount') {
|
||||||
return (
|
return (
|
||||||
<TableRowCell key={name}>
|
<TableRowCell key={name}>
|
||||||
|
@ -203,6 +214,7 @@ AlbumRow.propTypes = {
|
||||||
artistId: PropTypes.number.isRequired,
|
artistId: PropTypes.number.isRequired,
|
||||||
monitored: PropTypes.bool.isRequired,
|
monitored: PropTypes.bool.isRequired,
|
||||||
releaseDate: PropTypes.string.isRequired,
|
releaseDate: PropTypes.string.isRequired,
|
||||||
|
mediumCount: PropTypes.number.isRequired,
|
||||||
duration: PropTypes.number.isRequired,
|
duration: PropTypes.number.isRequired,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
isSaving: PropTypes.bool,
|
isSaving: PropTypes.bool,
|
||||||
|
|
|
@ -47,10 +47,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.titleRow {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.titleContainer {
|
.titleContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -111,6 +119,11 @@
|
||||||
font-family: $monoSpaceFontFamily;
|
font-family: $monoSpaceFontFamily;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.overview {
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.contentContainer {
|
.contentContainer {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
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 { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
import { align, icons, kinds, sizes, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import fonts from 'Styles/Variables/fonts';
|
||||||
import HeartRating from 'Components/HeartRating';
|
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';
|
||||||
|
@ -31,33 +32,8 @@ import ArtistTagsConnector from './ArtistTagsConnector';
|
||||||
import ArtistDetailsLinks from './ArtistDetailsLinks';
|
import ArtistDetailsLinks from './ArtistDetailsLinks';
|
||||||
import styles from './ArtistDetails.css';
|
import styles from './ArtistDetails.css';
|
||||||
|
|
||||||
const albumTypes = [
|
const defaultFontSize = parseInt(fonts.defaultFontSize);
|
||||||
{
|
const lineHeight = parseFloat(fonts.lineHeight);
|
||||||
name: 'album',
|
|
||||||
label: 'Album',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ep',
|
|
||||||
label: 'EP',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'single',
|
|
||||||
label: 'Single',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'broadcast',
|
|
||||||
label: 'Broadcast',
|
|
||||||
isVisible: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'other',
|
|
||||||
label: 'Other',
|
|
||||||
isVisible: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function getFanartUrl(images) {
|
function getFanartUrl(images) {
|
||||||
const fanartImage = _.find(images, { coverType: 'fanart' });
|
const fanartImage = _.find(images, { coverType: 'fanart' });
|
||||||
|
@ -174,6 +150,7 @@ class ArtistDetails extends Component {
|
||||||
links,
|
links,
|
||||||
images,
|
images,
|
||||||
albums,
|
albums,
|
||||||
|
primaryAlbumTypes,
|
||||||
alternateTitles,
|
alternateTitles,
|
||||||
tags,
|
tags,
|
||||||
isRefreshing,
|
isRefreshing,
|
||||||
|
@ -475,11 +452,9 @@ class ArtistDetails extends Component {
|
||||||
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.overview}>
|
||||||
<div>
|
|
||||||
<TextTruncate
|
<TextTruncate
|
||||||
truncateText="…"
|
line={Math.floor(200 / (defaultFontSize * lineHeight))}
|
||||||
line={8}
|
|
||||||
text={overview}
|
text={overview}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -495,26 +470,27 @@ class ArtistDetails extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && episodesError &&
|
!isFetching && episodesError &&
|
||||||
<div>Loading episodes failed</div>
|
<div>Loading albums failed</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
!isFetching && trackFilesError &&
|
!isFetching && trackFilesError &&
|
||||||
<div>Loading episode files failed</div>
|
<div>Loading track files failed</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !!albumTypes.length &&
|
isPopulated && !!primaryAlbumTypes.length &&
|
||||||
<div>
|
<div>
|
||||||
{
|
{
|
||||||
albumTypes.slice(0).map((season) => {
|
primaryAlbumTypes.slice(0).map((albumType) => {
|
||||||
return (
|
return (
|
||||||
<ArtistDetailsSeasonConnector
|
<ArtistDetailsSeasonConnector
|
||||||
key={season.name}
|
key={albumType}
|
||||||
artistId={id}
|
artistId={id}
|
||||||
label={season.label}
|
name={albumType}
|
||||||
{...season}
|
label={albumType}
|
||||||
isExpanded={expandedState[season.name]}
|
{...albumType}
|
||||||
|
isExpanded={expandedState[albumType]}
|
||||||
onExpandPress={this.onExpandPress}
|
onExpandPress={this.onExpandPress}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -570,6 +546,7 @@ ArtistDetails.propTypes = {
|
||||||
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
links: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
albums: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
primaryAlbumTypes: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
alternateTitles: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
tags: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||||
isRefreshing: PropTypes.bool.isRequired,
|
isRefreshing: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -47,7 +47,9 @@ $hoverScale: 1.05;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
display: flex;
|
||||||
flex: 1 0 1px;
|
flex: 1 0 1px;
|
||||||
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
}
|
}
|
||||||
|
@ -75,6 +77,7 @@ $hoverScale: 1.05;
|
||||||
.details {
|
.details {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
flex: 1 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview {
|
.overview {
|
||||||
|
@ -82,6 +85,7 @@ $hoverScale: 1.05;
|
||||||
|
|
||||||
flex: 0 1 1000px;
|
flex: 0 1 1000px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Truncate from 'react-truncate';
|
import TextTruncate from 'react-text-truncate';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import dimensions from 'Styles/Variables/dimensions';
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import fonts from 'Styles/Variables/fonts';
|
import fonts from 'Styles/Variables/fonts';
|
||||||
|
@ -176,16 +176,15 @@ class ArtistIndexOverview extends Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.details}>
|
<div className={styles.details}>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className={styles.overview}
|
className={styles.overview}
|
||||||
style={{
|
|
||||||
maxHeight: `${height}px`
|
|
||||||
}}
|
|
||||||
to={link}
|
to={link}
|
||||||
>
|
>
|
||||||
<Truncate lines={Math.floor(height / (defaultFontSize * lineHeight))}>
|
<TextTruncate
|
||||||
{overview}
|
line={Math.floor(height / (defaultFontSize * lineHeight))}
|
||||||
</Truncate>
|
text={overview}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<ArtistIndexOverviewInfo
|
<ArtistIndexOverviewInfo
|
||||||
|
|
|
@ -56,7 +56,9 @@ class CalendarConnector extends Component {
|
||||||
const albumIds = selectUniqueIds(items, 'id');
|
const albumIds = selectUniqueIds(items, 'id');
|
||||||
// const trackFileIds = selectUniqueIds(items, 'trackFileId');
|
// const trackFileIds = selectUniqueIds(items, 'trackFileId');
|
||||||
|
|
||||||
this.props.fetchQueueDetails({ albumIds });
|
if (items.length) {
|
||||||
|
this.props.fetchQueueDetails({ albumIds });
|
||||||
|
}
|
||||||
|
|
||||||
// if (trackFileIds.length) {
|
// if (trackFileIds.length) {
|
||||||
// this.props.fetchTrackFiles({ trackFileIds });
|
// this.props.fetchTrackFiles({ trackFileIds });
|
||||||
|
|
|
@ -27,6 +27,10 @@
|
||||||
background-color: #aaa;
|
background-color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.isHidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.isMobile {
|
.isMobile {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
border-bottom: 1px solid $borderColor;
|
border-bottom: 1px solid $borderColor;
|
||||||
|
|
|
@ -28,6 +28,7 @@ class EnhancedSelectInputOption extends Component {
|
||||||
className,
|
className,
|
||||||
isSelected,
|
isSelected,
|
||||||
isDisabled,
|
isDisabled,
|
||||||
|
isHidden,
|
||||||
isMobile,
|
isMobile,
|
||||||
children
|
children
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -38,6 +39,7 @@ class EnhancedSelectInputOption extends Component {
|
||||||
className,
|
className,
|
||||||
isSelected && styles.isSelected,
|
isSelected && styles.isSelected,
|
||||||
isDisabled && styles.isDisabled,
|
isDisabled && styles.isDisabled,
|
||||||
|
isHidden && styles.isHidden,
|
||||||
isMobile && styles.isMobile
|
isMobile && styles.isMobile
|
||||||
)}
|
)}
|
||||||
component="div"
|
component="div"
|
||||||
|
@ -64,6 +66,7 @@ EnhancedSelectInputOption.propTypes = {
|
||||||
id: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
isSelected: PropTypes.bool.isRequired,
|
isSelected: PropTypes.bool.isRequired,
|
||||||
isDisabled: PropTypes.bool.isRequired,
|
isDisabled: PropTypes.bool.isRequired,
|
||||||
|
isHidden: PropTypes.bool.isRequired,
|
||||||
isMobile: PropTypes.bool.isRequired,
|
isMobile: PropTypes.bool.isRequired,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
onSelect: PropTypes.func.isRequired
|
onSelect: PropTypes.func.isRequired
|
||||||
|
@ -71,7 +74,8 @@ EnhancedSelectInputOption.propTypes = {
|
||||||
|
|
||||||
EnhancedSelectInputOption.defaultProps = {
|
EnhancedSelectInputOption.defaultProps = {
|
||||||
className: styles.option,
|
className: styles.option,
|
||||||
isDisabled: false
|
isDisabled: false,
|
||||||
|
isHidden: false
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EnhancedSelectInputOption;
|
export default EnhancedSelectInputOption;
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
|
|
||||||
/* Sizes */
|
/* Sizes */
|
||||||
|
|
||||||
|
.extraSmall {
|
||||||
|
max-width: $formGroupExtraSmallWidth;
|
||||||
|
}
|
||||||
|
|
||||||
.small {
|
.small {
|
||||||
max-width: $formGroupSmallWidth;
|
max-width: $formGroupSmallWidth;
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ function FormGroup(props) {
|
||||||
FormGroup.propTypes = {
|
FormGroup.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
size: PropTypes.string.isRequired,
|
size: PropTypes.oneOf(sizes.all).isRequired,
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
advancedSettings: PropTypes.bool.isRequired,
|
||||||
isAdvanced: PropTypes.bool.isRequired
|
isAdvanced: PropTypes.bool.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
.label {
|
.label {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
flex: 0 0 $formLabelWidth;
|
|
||||||
margin-right: $formLabelRightMarginWidth;
|
margin-right: $formLabelRightMarginWidth;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
line-height: 35px;
|
line-height: 35px;
|
||||||
|
@ -20,3 +19,12 @@
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
flex: 0 0 $formLabelSmallWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
.large {
|
||||||
|
flex: 0 0 $formLabelLargeWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
import styles from './FormLabel.css';
|
import styles from './FormLabel.css';
|
||||||
|
|
||||||
function FormLabel({
|
function FormLabel({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
errorClassName,
|
errorClassName,
|
||||||
|
size,
|
||||||
name,
|
name,
|
||||||
hasError,
|
hasError,
|
||||||
isAdvanced,
|
isAdvanced,
|
||||||
|
@ -17,6 +19,7 @@ function FormLabel({
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
|
styles[size],
|
||||||
hasError && errorClassName,
|
hasError && errorClassName,
|
||||||
isAdvanced && styles.isAdvanced
|
isAdvanced && styles.isAdvanced
|
||||||
)}
|
)}
|
||||||
|
@ -31,6 +34,7 @@ FormLabel.propTypes = {
|
||||||
children: PropTypes.node.isRequired,
|
children: PropTypes.node.isRequired,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
errorClassName: PropTypes.string,
|
errorClassName: PropTypes.string,
|
||||||
|
size: PropTypes.oneOf(sizes.all),
|
||||||
name: PropTypes.string,
|
name: PropTypes.string,
|
||||||
hasError: PropTypes.bool,
|
hasError: PropTypes.bool,
|
||||||
isAdvanced: PropTypes.bool.isRequired
|
isAdvanced: PropTypes.bool.isRequired
|
||||||
|
@ -39,7 +43,8 @@ FormLabel.propTypes = {
|
||||||
FormLabel.defaultProps = {
|
FormLabel.defaultProps = {
|
||||||
className: styles.label,
|
className: styles.label,
|
||||||
errorClassName: styles.hasError,
|
errorClassName: styles.hasError,
|
||||||
isAdvanced: false
|
isAdvanced: false,
|
||||||
|
size: sizes.LARGE
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FormLabel;
|
export default FormLabel;
|
||||||
|
|
|
@ -22,20 +22,19 @@ class RootFolderSelectInput extends Component {
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
values,
|
|
||||||
isSaving,
|
isSaving,
|
||||||
saveError,
|
saveError,
|
||||||
onChange
|
onChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
const newRootFolderPath = this.state.newRootFolderPath;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevProps.isSaving &&
|
prevProps.isSaving &&
|
||||||
!isSaving &&
|
!isSaving &&
|
||||||
!saveError &&
|
!saveError &&
|
||||||
values.length - prevProps.values.length === 1
|
newRootFolderPath
|
||||||
) {
|
) {
|
||||||
const newRootFolderPath = this.state.newRootFolderPath;
|
|
||||||
|
|
||||||
onChange({ name, value: newRootFolderPath });
|
onChange({ name, value: newRootFolderPath });
|
||||||
this.setState({ newRootFolderPath: '' });
|
this.setState({ newRootFolderPath: '' });
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,8 @@ function createMapStateToProps() {
|
||||||
values.push({
|
values.push({
|
||||||
key: '',
|
key: '',
|
||||||
value: '',
|
value: '',
|
||||||
isDisabled: true
|
isDisabled: true,
|
||||||
|
isHidden: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,6 +65,18 @@ class RootFolderSelectInputConnector extends Component {
|
||||||
//
|
//
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
const {
|
||||||
|
value,
|
||||||
|
values,
|
||||||
|
onChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
if (value == null && values[0].key === '') {
|
||||||
|
onChange({ name, value: '' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {
|
const {
|
||||||
name,
|
name,
|
||||||
|
|
|
@ -12,9 +12,9 @@ const messages = [
|
||||||
'Hum something loud while others stare',
|
'Hum something loud while others stare',
|
||||||
'Loading humorous message... Please Wait',
|
'Loading humorous message... Please Wait',
|
||||||
'I could\'ve been faster in Python',
|
'I could\'ve been faster in Python',
|
||||||
'Don\'t forget to rewind your episodes',
|
'Don\'t forget to rewind your tracks',
|
||||||
'Congratulations! you are the 1000th visitor.',
|
'Congratulations! you are the 1000th visitor.',
|
||||||
'HELP!, I\'m being held hostage and forced to write these stupid lines!',
|
'HELP! I\'m being held hostage and forced to write these stupid lines!',
|
||||||
'RE-calibrating the internet...',
|
'RE-calibrating the internet...',
|
||||||
'I\'ll be here all week',
|
'I\'ll be here all week',
|
||||||
'Don\'t forget to tip your waitress',
|
'Don\'t forget to tip your waitress',
|
||||||
|
|
|
@ -51,6 +51,18 @@
|
||||||
width: 1080px;
|
width: 1080px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.extraLarge {
|
||||||
|
composes: modal;
|
||||||
|
|
||||||
|
width: 1440px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointExtraLarge) {
|
||||||
|
.modal.extraLarge {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointLarge) {
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
.modal.large {
|
.modal.large {
|
||||||
width: 90%;
|
width: 90%;
|
||||||
|
@ -71,9 +83,10 @@
|
||||||
|
|
||||||
.modal.small,
|
.modal.small,
|
||||||
.modal.medium,
|
.modal.medium,
|
||||||
.modal.large {
|
.modal.large,
|
||||||
|
.modal.extraLarge {
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100% !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,7 @@ class Modal extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
className,
|
className,
|
||||||
|
style,
|
||||||
backdropClassName,
|
backdropClassName,
|
||||||
size,
|
size,
|
||||||
children,
|
children,
|
||||||
|
@ -166,6 +167,7 @@ class Modal extends Component {
|
||||||
className,
|
className,
|
||||||
styles[size]
|
styles[size]
|
||||||
)}
|
)}
|
||||||
|
style={style}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
@ -180,6 +182,7 @@ class Modal extends Component {
|
||||||
|
|
||||||
Modal.propTypes = {
|
Modal.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
backdropClassName: PropTypes.string,
|
backdropClassName: PropTypes.string,
|
||||||
size: PropTypes.oneOf(sizes.all),
|
size: PropTypes.oneOf(sizes.all),
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
$modalBodyPadding: 30px;
|
|
||||||
|
|
||||||
.modalBody {
|
.modalBody {
|
||||||
flex: 1 0 1px;
|
flex: 1 0 1px;
|
||||||
padding: $modalBodyPadding;
|
padding: $modalBodyPadding;
|
||||||
|
|
|
@ -23,13 +23,13 @@ class PageHeader extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.bindShortcut(shortcuts.OPEN_KEYBOARD_SHORTCUTS_MODAL.key, this.openKeyboardShortcutsModal);
|
this.props.bindShortcut(shortcuts.OPEN_KEYBOARD_SHORTCUTS_MODAL.key, this.onOpenKeyboardShortcutsModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
openKeyboardShortcutsModal = () => {
|
onOpenKeyboardShortcutsModal = () => {
|
||||||
this.setState({ isKeyboardShortcutsModalOpen: true });
|
this.setState({ isKeyboardShortcutsModalOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +76,9 @@ class PageHeader extends Component {
|
||||||
name={icons.HEART}
|
name={icons.HEART}
|
||||||
to="https://lidarr.audio/donate.html"
|
to="https://lidarr.audio/donate.html"
|
||||||
/>
|
/>
|
||||||
<PageHeaderActionsMenuConnector />
|
<PageHeaderActionsMenuConnector
|
||||||
|
onKeyboardShortcutsPress={this.onOpenKeyboardShortcutsModal}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<KeyboardShortcutsModal
|
<KeyboardShortcutsModal
|
||||||
|
|
|
@ -11,6 +11,7 @@ import styles from './PageHeaderActionsMenu.css';
|
||||||
function PageHeaderActionsMenu(props) {
|
function PageHeaderActionsMenu(props) {
|
||||||
const {
|
const {
|
||||||
formsAuth,
|
formsAuth,
|
||||||
|
onKeyboardShortcutsPress,
|
||||||
onRestartPress,
|
onRestartPress,
|
||||||
onShutdownPress
|
onShutdownPress
|
||||||
} = props;
|
} = props;
|
||||||
|
@ -25,6 +26,16 @@ function PageHeaderActionsMenu(props) {
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
<MenuContent>
|
<MenuContent>
|
||||||
|
<MenuItem onPress={onKeyboardShortcutsPress}>
|
||||||
|
<Icon
|
||||||
|
className={styles.itemIcon}
|
||||||
|
name={icons.KEYBOARD}
|
||||||
|
/>
|
||||||
|
Keyboard Shortcuts
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<div className={styles.separator} />
|
||||||
|
|
||||||
<MenuItem onPress={onRestartPress}>
|
<MenuItem onPress={onRestartPress}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.itemIcon}
|
className={styles.itemIcon}
|
||||||
|
@ -68,6 +79,7 @@ function PageHeaderActionsMenu(props) {
|
||||||
|
|
||||||
PageHeaderActionsMenu.propTypes = {
|
PageHeaderActionsMenu.propTypes = {
|
||||||
formsAuth: PropTypes.bool.isRequired,
|
formsAuth: PropTypes.bool.isRequired,
|
||||||
|
onKeyboardShortcutsPress: PropTypes.func.isRequired,
|
||||||
onRestartPress: PropTypes.func.isRequired,
|
onRestartPress: PropTypes.func.isRequired,
|
||||||
onShutdownPress: PropTypes.func.isRequired
|
onShutdownPress: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -26,6 +26,14 @@ function getState(status) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAppDisconnected(disconnectedTime) {
|
||||||
|
if (!disconnectedTime) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.floor(new Date().getTime() / 1000) - disconnectedTime > 180;
|
||||||
|
}
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.app.isReconnecting,
|
(state) => state.app.isReconnecting,
|
||||||
|
@ -66,6 +74,7 @@ class SignalRConnector extends Component {
|
||||||
this.signalRconnection = null;
|
this.signalRconnection = null;
|
||||||
this.retryInterval = 5;
|
this.retryInterval = 5;
|
||||||
this.retryTimeoutId = null;
|
this.retryTimeoutId = null;
|
||||||
|
this.disconnectedTime = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -90,7 +99,7 @@ class SignalRConnector extends Component {
|
||||||
// Control
|
// Control
|
||||||
|
|
||||||
retryConnection = () => {
|
retryConnection = () => {
|
||||||
if (this.retryInterval >= 30) {
|
if (isAppDisconnected(this.disconnectedTime)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isDisconnected: true
|
isDisconnected: true
|
||||||
});
|
});
|
||||||
|
@ -290,6 +299,9 @@ class SignalRConnector extends Component {
|
||||||
console.log(`SignalR: ${state}`);
|
console.log(`SignalR: ${state}`);
|
||||||
|
|
||||||
if (state === 'connected') {
|
if (state === 'connected') {
|
||||||
|
// Clear disconnected time
|
||||||
|
this.disconnectedTime = null;
|
||||||
|
|
||||||
// Repopulate the page (if a repopulator is set) to ensure things
|
// Repopulate the page (if a repopulator is set) to ensure things
|
||||||
// are in sync after reconnecting.
|
// are in sync after reconnecting.
|
||||||
|
|
||||||
|
@ -322,6 +334,10 @@ class SignalRConnector extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.disconnectedTime) {
|
||||||
|
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
this.props.setAppValue({
|
this.props.setAppValue({
|
||||||
isReconnecting: true
|
isReconnecting: true
|
||||||
});
|
});
|
||||||
|
@ -332,11 +348,14 @@ class SignalRConnector extends Component {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.disconnectedTime) {
|
||||||
|
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
|
||||||
|
}
|
||||||
|
|
||||||
this.props.setAppValue({
|
this.props.setAppValue({
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
isReconnecting: true
|
isReconnecting: true,
|
||||||
// Don't set isDisconnected yet, it'll be set it if it's disconnected
|
isDisconnected: isAppDisconnected(this.disconnectedTime)
|
||||||
// for ~105 seconds (retry interval reaches 30 seconds)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.retryConnection();
|
this.retryConnection();
|
||||||
|
|
|
@ -8,7 +8,7 @@ import TableOptionsColumn from './TableOptionsColumn';
|
||||||
import styles from './TableOptionsColumnDragPreview.css';
|
import styles from './TableOptionsColumnDragPreview.css';
|
||||||
|
|
||||||
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
||||||
const formLabelWidth = parseInt(dimensions.formLabelWidth);
|
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
|
||||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class TableOptionsColumnDragPreview extends Component {
|
||||||
// list item and the preview is wider than the drag handle.
|
// list item and the preview is wider than the drag handle.
|
||||||
|
|
||||||
const { x, y } = currentOffset;
|
const { x, y } = currentOffset;
|
||||||
const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth;
|
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
|
|
|
@ -53,6 +53,14 @@ class Popover extends Component {
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: false
|
isOpen: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._closeTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._closeTimeout) {
|
||||||
|
this._closeTimeout = clearTimeout(this._closeTimeout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -63,11 +71,17 @@ class Popover extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseEnter = () => {
|
onMouseEnter = () => {
|
||||||
|
if (this._closeTimeout) {
|
||||||
|
this._closeTimeout = clearTimeout(this._closeTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({ isOpen: true });
|
this.setState({ isOpen: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
onMouseLeave = () => {
|
onMouseLeave = () => {
|
||||||
this.setState({ isOpen: false });
|
this._closeTimeout = setTimeout(() => {
|
||||||
|
this.setState({ isOpen: false });
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -98,24 +112,28 @@ class Popover extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
this.state.isOpen &&
|
this.state.isOpen &&
|
||||||
<div className={styles.popoverContainer}>
|
<div
|
||||||
<div className={styles.popover}>
|
className={styles.popoverContainer}
|
||||||
<div
|
onMouseEnter={this.onMouseEnter}
|
||||||
className={classNames(
|
onMouseLeave={this.onMouseLeave}
|
||||||
styles.arrow,
|
>
|
||||||
styles[position]
|
<div className={styles.popover}>
|
||||||
)}
|
<div
|
||||||
/>
|
className={classNames(
|
||||||
|
styles.arrow,
|
||||||
|
styles[position]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className={styles.title}>
|
<div className={styles.title}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
{body}
|
{body}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</TetherComponent>
|
</TetherComponent>
|
||||||
);
|
);
|
||||||
|
|
|
@ -50,11 +50,17 @@ class Tooltip extends Component {
|
||||||
constructor(props, context) {
|
constructor(props, context) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this._closeTimeout = null;
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isOpen: false
|
isOpen: false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._closeTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
if (this._closeTimeout) {
|
||||||
|
this._closeTimeout = clearTimeout(this._closeTimeout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
@ -83,6 +89,7 @@ class Tooltip extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
className,
|
||||||
anchor,
|
anchor,
|
||||||
tooltip,
|
tooltip,
|
||||||
kind,
|
kind,
|
||||||
|
@ -97,6 +104,7 @@ class Tooltip extends Component {
|
||||||
{...tetherOptions[position]}
|
{...tetherOptions[position]}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
|
className={className}
|
||||||
// onClick={this.onClick}
|
// onClick={this.onClick}
|
||||||
onMouseEnter={this.onMouseEnter}
|
onMouseEnter={this.onMouseEnter}
|
||||||
onMouseLeave={this.onMouseLeave}
|
onMouseLeave={this.onMouseLeave}
|
||||||
|
@ -137,6 +145,7 @@ class Tooltip extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
Tooltip.propTypes = {
|
Tooltip.propTypes = {
|
||||||
|
className: PropTypes.string,
|
||||||
anchor: PropTypes.node.isRequired,
|
anchor: PropTypes.node.isRequired,
|
||||||
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
tooltip: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
|
||||||
kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]),
|
kind: PropTypes.oneOf([kinds.DEFAULT, kinds.INVERSE]),
|
||||||
|
|
|
@ -36,11 +36,13 @@ export const FILE = 'fa fa-file-o';
|
||||||
export const FILTER = 'fa fa-filter';
|
export const FILTER = 'fa fa-filter';
|
||||||
export const FOLDER = 'fa fa-folder-o';
|
export const FOLDER = 'fa fa-folder-o';
|
||||||
export const FOLDER_OPEN = 'fa fa-folder-open';
|
export const FOLDER_OPEN = 'fa fa-folder-open';
|
||||||
|
export const GROUP = 'fa fa-object-group';
|
||||||
export const HEALTH = 'fa fa-medkit';
|
export const HEALTH = 'fa fa-medkit';
|
||||||
export const HEART = 'fa fa-heart';
|
export const HEART = 'fa fa-heart';
|
||||||
export const HOUSEKEEPING = 'fa fa-home';
|
export const HOUSEKEEPING = 'fa fa-home';
|
||||||
export const INFO = 'fa fa-info-circle';
|
export const INFO = 'fa fa-info-circle';
|
||||||
export const INTERACTIVE = 'fa fa-user';
|
export const INTERACTIVE = 'fa fa-user';
|
||||||
|
export const KEYBOARD = 'fa fa-keyboard-o';
|
||||||
export const LOGOUT = 'fa fa-sign-out';
|
export const LOGOUT = 'fa fa-sign-out';
|
||||||
export const MISSING = 'fa fa-exclamation-triangle';
|
export const MISSING = 'fa fa-exclamation-triangle';
|
||||||
export const MONITORED = 'fa fa-bookmark';
|
export const MONITORED = 'fa fa-bookmark';
|
||||||
|
@ -82,6 +84,7 @@ export const SUBTRACT = 'fa fa-minus';
|
||||||
export const SYSTEM = 'fa fa-laptop';
|
export const SYSTEM = 'fa fa-laptop';
|
||||||
export const TAGS = 'fa fa-tags';
|
export const TAGS = 'fa fa-tags';
|
||||||
export const TBA = 'fa fa-question-circle';
|
export const TBA = 'fa fa-question-circle';
|
||||||
|
export const UNGROUP = 'fa fa-object-ungroup';
|
||||||
export const UNKNOWN = 'fa fa-question';
|
export const UNKNOWN = 'fa fa-question';
|
||||||
export const UNMONITORED = 'fa fa-bookmark-o';
|
export const UNMONITORED = 'fa fa-bookmark-o';
|
||||||
export const UPDATE = 'fa fa-retweet';
|
export const UPDATE = 'fa fa-retweet';
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
|
export const EXTRA_SMALL = 'extraSmall';
|
||||||
export const SMALL = 'small';
|
export const SMALL = 'small';
|
||||||
export const MEDIUM = 'medium';
|
export const MEDIUM = 'medium';
|
||||||
export const LARGE = 'large';
|
export const LARGE = 'large';
|
||||||
|
export const EXTRA_LARGE = 'extraLarge';
|
||||||
|
|
||||||
export const all = [SMALL, MEDIUM, LARGE];
|
export const all = [EXTRA_SMALL, SMALL, MEDIUM, LARGE, EXTRA_LARGE];
|
||||||
|
|
|
@ -4,8 +4,15 @@
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quality {
|
.quality,
|
||||||
|
.language {
|
||||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
composes: label from 'Components/Label.css';
|
||||||
|
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
|
@ -238,6 +238,7 @@ class InteractiveImportRow extends Component {
|
||||||
onPress={this.onSelectQualityPress}
|
onPress={this.onSelectQualityPress}
|
||||||
>
|
>
|
||||||
<EpisodeQuality
|
<EpisodeQuality
|
||||||
|
className={styles.label}
|
||||||
quality={quality}
|
quality={quality}
|
||||||
/>
|
/>
|
||||||
</TableRowCellButton>
|
</TableRowCellButton>
|
||||||
|
@ -247,6 +248,7 @@ class InteractiveImportRow extends Component {
|
||||||
onPress={this.onSelectLanguagePress}
|
onPress={this.onSelectLanguagePress}
|
||||||
>
|
>
|
||||||
<EpisodeLanguage
|
<EpisodeLanguage
|
||||||
|
className={styles.label}
|
||||||
language={language}
|
language={language}
|
||||||
/>
|
/>
|
||||||
</TableRowCellButton>
|
</TableRowCellButton>
|
||||||
|
|
|
@ -70,10 +70,10 @@ class SelectQualityModalContent extends Component {
|
||||||
real
|
real
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
const qualityOptions = items.map(({ quality }) => {
|
const qualityOptions = items.map(({ id, name }) => {
|
||||||
return {
|
return {
|
||||||
key: quality.id,
|
key: id,
|
||||||
value: quality.name
|
value: name
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,6 +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 getQualities from 'Utilities/Quality/getQualities';
|
||||||
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
import { fetchQualityProfileSchema } from 'Store/Actions/settingsActions';
|
||||||
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
import { updateInteractiveImportItem } from 'Store/Actions/interactiveImportActions';
|
||||||
import SelectQualityModalContent from './SelectQualityModalContent';
|
import SelectQualityModalContent from './SelectQualityModalContent';
|
||||||
|
@ -22,7 +23,7 @@ function createMapStateToProps() {
|
||||||
isFetching,
|
isFetching,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
error,
|
error,
|
||||||
items: schema.items || []
|
items: getQualities(schema.items)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -90,6 +90,15 @@ class NamingModal extends Component {
|
||||||
{ token: '{Album_CleanTitle}', example: 'Album_Title' }
|
{ token: '{Album_CleanTitle}', example: 'Album_Title' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mediumTokens = [
|
||||||
|
{ token: '{medium:0}', example: '1' },
|
||||||
|
{ token: '{medium:00}', example: '01' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const mediumFormatTokens = [
|
||||||
|
{ token: '{Medium Format}', example: 'CD' }
|
||||||
|
];
|
||||||
|
|
||||||
const trackTokens = [
|
const trackTokens = [
|
||||||
{ token: '{track:0}', example: '1' },
|
{ token: '{track:0}', example: '1' },
|
||||||
{ token: '{track:00}', example: '01' }
|
{ token: '{track:00}', example: '01' }
|
||||||
|
@ -260,6 +269,48 @@ class NamingModal extends Component {
|
||||||
{
|
{
|
||||||
track &&
|
track &&
|
||||||
<div>
|
<div>
|
||||||
|
<FieldSet legend="Medium">
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{
|
||||||
|
mediumTokens.map(({ token, example }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
tokenCase={this.state.case}
|
||||||
|
onInputChange={onInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
|
<FieldSet legend="Medium Format">
|
||||||
|
<div className={styles.groups}>
|
||||||
|
{
|
||||||
|
mediumFormatTokens.map(({ token, example }) => {
|
||||||
|
return (
|
||||||
|
<NamingOption
|
||||||
|
key={token}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
token={token}
|
||||||
|
example={example}
|
||||||
|
tokenCase={this.state.case}
|
||||||
|
onInputChange={onInputChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</FieldSet>
|
||||||
|
|
||||||
<FieldSet legend="Track">
|
<FieldSet legend="Track">
|
||||||
<div className={styles.groups}>
|
<div className={styles.groups}>
|
||||||
{
|
{
|
||||||
|
|
|
@ -8,7 +8,7 @@ import LanguageProfileItem from './LanguageProfileItem';
|
||||||
import styles from './LanguageProfileItemDragPreview.css';
|
import styles from './LanguageProfileItemDragPreview.css';
|
||||||
|
|
||||||
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
||||||
const formLabelWidth = parseInt(dimensions.formLabelWidth);
|
const formLabelLargeWidth = parseInt(dimensions.formLabelLargeWidth);
|
||||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class LanguageProfileItemDragPreview extends Component {
|
||||||
// list item and the preview is wider than the drag handle.
|
// list item and the preview is wider than the drag handle.
|
||||||
|
|
||||||
const { x, y } = currentOffset;
|
const { x, y } = currentOffset;
|
||||||
const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth;
|
const handleOffset = formGroupSmallWidth - formLabelLargeWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
|
|
|
@ -1,20 +1,56 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import EditQualityProfileModalContentConnector from './EditQualityProfileModalContentConnector';
|
import EditQualityProfileModalContentConnector from './EditQualityProfileModalContentConnector';
|
||||||
|
|
||||||
function EditQualityProfileModal({ isOpen, onModalClose, ...otherProps }) {
|
class EditQualityProfileModal extends Component {
|
||||||
return (
|
|
||||||
<Modal
|
//
|
||||||
isOpen={isOpen}
|
// Lifecycle
|
||||||
onModalClose={onModalClose}
|
|
||||||
>
|
constructor(props, context) {
|
||||||
<EditQualityProfileModalContentConnector
|
super(props, context);
|
||||||
{...otherProps}
|
|
||||||
|
this.state = {
|
||||||
|
height: 'auto'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onContentHeightChange = (height) => {
|
||||||
|
if (this.state.height === 'auto' || height > this.state.height) {
|
||||||
|
this.setState({ height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onModalClose,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
style={{ height: `${this.state.height}px` }}
|
||||||
|
isOpen={isOpen}
|
||||||
|
size={sizes.EXTRA_LARGE}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
/>
|
>
|
||||||
</Modal>
|
<EditQualityProfileModalContentConnector
|
||||||
);
|
{...otherProps}
|
||||||
|
onContentHeightChange={this.onContentHeightChange}
|
||||||
|
onModalClose={onModalClose}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditQualityProfileModal.propTypes = {
|
EditQualityProfileModal.propTypes = {
|
||||||
|
|
|
@ -1,3 +1,18 @@
|
||||||
|
.formGroupsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formGroupWrapper {
|
||||||
|
flex: 0 0 calc($formGroupSmallWidth - 100px);
|
||||||
|
}
|
||||||
|
|
||||||
.deleteButtonContainer {
|
.deleteButtonContainer {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: $breakpointLarge) {
|
||||||
|
.formGroupsContainer {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React, { Component } from 'react';
|
||||||
import { inputTypes, kinds } from 'Helpers/Props';
|
import Measure from 'react-measure';
|
||||||
|
import { inputTypes, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import dimensions from 'Styles/Variables/dimensions';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
@ -15,123 +17,223 @@ import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||||
import QualityProfileItems from './QualityProfileItems';
|
import QualityProfileItems from './QualityProfileItems';
|
||||||
import styles from './EditQualityProfileModalContent.css';
|
import styles from './EditQualityProfileModalContent.css';
|
||||||
|
|
||||||
function EditQualityProfileModalContent(props) {
|
const MODAL_BODY_PADDING = parseInt(dimensions.modalBodyPadding);
|
||||||
const {
|
|
||||||
isFetching,
|
|
||||||
error,
|
|
||||||
isSaving,
|
|
||||||
saveError,
|
|
||||||
qualities,
|
|
||||||
item,
|
|
||||||
isInUse,
|
|
||||||
onInputChange,
|
|
||||||
onCutoffChange,
|
|
||||||
onSavePress,
|
|
||||||
onModalClose,
|
|
||||||
onDeleteQualityProfilePress,
|
|
||||||
...otherProps
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
class EditQualityProfileModalContent extends Component {
|
||||||
id,
|
|
||||||
name,
|
|
||||||
cutoff,
|
|
||||||
items
|
|
||||||
} = item;
|
|
||||||
|
|
||||||
return (
|
//
|
||||||
<ModalContent onModalClose={onModalClose}>
|
// Lifecycle
|
||||||
<ModalHeader>
|
|
||||||
{id ? 'Edit Quality Profile' : 'Add Quality Profile'}
|
|
||||||
</ModalHeader>
|
|
||||||
|
|
||||||
<ModalBody>
|
constructor(props, context) {
|
||||||
{
|
super(props, context);
|
||||||
isFetching &&
|
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
this.state = {
|
||||||
!isFetching && !!error &&
|
headerHeight: 0,
|
||||||
<div>Unable to add a new quality profile, please try again.</div>
|
bodyHeight: 0,
|
||||||
}
|
footerHeight: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
{
|
componentDidUpdate(prevProps, prevState) {
|
||||||
!isFetching && !error &&
|
const {
|
||||||
<Form
|
headerHeight,
|
||||||
{...otherProps}
|
bodyHeight,
|
||||||
>
|
footerHeight
|
||||||
<FormGroup>
|
} = this.state;
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
|
|
||||||
<FormInputGroup
|
if (
|
||||||
type={inputTypes.TEXT}
|
headerHeight > 0 &&
|
||||||
name="name"
|
bodyHeight > 0 &&
|
||||||
{...name}
|
footerHeight > 0 &&
|
||||||
onChange={onInputChange}
|
(
|
||||||
/>
|
headerHeight !== prevState.headerHeight ||
|
||||||
</FormGroup>
|
bodyHeight !== prevState.bodyHeight ||
|
||||||
|
footerHeight !== prevState.footerHeight
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const padding = MODAL_BODY_PADDING * 2;
|
||||||
|
|
||||||
<FormGroup>
|
this.props.onContentHeightChange(
|
||||||
<FormLabel>Cutoff</FormLabel>
|
headerHeight + bodyHeight + footerHeight + padding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
<FormInputGroup
|
//
|
||||||
type={inputTypes.SELECT}
|
// Listeners
|
||||||
name="cutoff"
|
|
||||||
{...cutoff}
|
|
||||||
value={cutoff ? cutoff.value.id : 0}
|
|
||||||
values={qualities}
|
|
||||||
helpText="Once this quality is reached Lidarr will no longer download episodes"
|
|
||||||
onChange={onCutoffChange}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<QualityProfileItems
|
onHeaderMeasure = ({ height }) => {
|
||||||
qualityProfileItems={items.value}
|
if (height > this.state.headerHeight) {
|
||||||
errors={items.errors}
|
this.setState({ headerHeight: height });
|
||||||
warnings={items.warnings}
|
}
|
||||||
{...otherProps}
|
}
|
||||||
/>
|
|
||||||
|
|
||||||
</Form>
|
onBodyMeasure = ({ height }) => {
|
||||||
}
|
|
||||||
</ModalBody>
|
if (height > this.state.bodyHeight) {
|
||||||
<ModalFooter>
|
this.setState({ bodyHeight: height });
|
||||||
{
|
}
|
||||||
id &&
|
}
|
||||||
<div
|
|
||||||
className={styles.deleteButtonContainer}
|
onFooterMeasure = ({ height }) => {
|
||||||
title={isInUse && 'Can\'t delete a quality profile that is attached to a artist'}
|
if (height > this.state.footerHeight) {
|
||||||
>
|
this.setState({ footerHeight: height });
|
||||||
<Button
|
}
|
||||||
kind={kinds.DANGER}
|
}
|
||||||
isDisabled={isInUse}
|
|
||||||
onPress={onDeleteQualityProfilePress}
|
//
|
||||||
>
|
// Render
|
||||||
Delete
|
|
||||||
</Button>
|
render() {
|
||||||
|
const {
|
||||||
|
editGroups,
|
||||||
|
isFetching,
|
||||||
|
error,
|
||||||
|
isSaving,
|
||||||
|
saveError,
|
||||||
|
qualities,
|
||||||
|
item,
|
||||||
|
isInUse,
|
||||||
|
onInputChange,
|
||||||
|
onCutoffChange,
|
||||||
|
onSavePress,
|
||||||
|
onModalClose,
|
||||||
|
onDeleteQualityProfilePress,
|
||||||
|
...otherProps
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
cutoff,
|
||||||
|
items
|
||||||
|
} = item;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<Measure
|
||||||
|
whitelist={['height']}
|
||||||
|
includeMargin={false}
|
||||||
|
onMeasure={this.onHeaderMeasure}
|
||||||
|
>
|
||||||
|
<ModalHeader>
|
||||||
|
{id ? 'Edit Quality Profile' : 'Add Quality Profile'}
|
||||||
|
</ModalHeader>
|
||||||
|
</Measure>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<Measure
|
||||||
|
whitelist={['height']}
|
||||||
|
onMeasure={this.onBodyMeasure}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !!error &&
|
||||||
|
<div>Unable to add a new quality profile, please try again.</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!isFetching && !error &&
|
||||||
|
<Form
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
<div className={styles.formGroupsContainer}>
|
||||||
|
<div className={styles.formGroupWrapper}>
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.small}>
|
||||||
|
Name
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.TEXT}
|
||||||
|
name="name"
|
||||||
|
{...name}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
|
<FormLabel size={sizes.small}>
|
||||||
|
Cutoff
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.SELECT}
|
||||||
|
name="cutoff"
|
||||||
|
{...cutoff}
|
||||||
|
values={qualities}
|
||||||
|
helpText="Once this quality is reached Sonarr will no longer download episodes"
|
||||||
|
onChange={onCutoffChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.formGroupWrapper}>
|
||||||
|
<QualityProfileItems
|
||||||
|
editGroups={editGroups}
|
||||||
|
qualityProfileItems={items.value}
|
||||||
|
errors={items.errors}
|
||||||
|
warnings={items.warnings}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
</Measure>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
<Button
|
<Measure
|
||||||
onPress={onModalClose}
|
whitelist={['height']}
|
||||||
|
includeMargin={false}
|
||||||
|
onMeasure={this.onFooterMeasure}
|
||||||
>
|
>
|
||||||
Cancel
|
<ModalFooter>
|
||||||
</Button>
|
{
|
||||||
|
id &&
|
||||||
|
<div
|
||||||
|
className={styles.deleteButtonContainer}
|
||||||
|
title={isInUse && 'Can\'t delete a quality profile that is attached to a series'}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
kind={kinds.DANGER}
|
||||||
|
isDisabled={isInUse}
|
||||||
|
onPress={onDeleteQualityProfilePress}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<SpinnerErrorButton
|
<Button
|
||||||
isSpinning={isSaving}
|
onPress={onModalClose}
|
||||||
error={saveError}
|
>
|
||||||
onPress={onSavePress}
|
Cancel
|
||||||
>
|
</Button>
|
||||||
Save
|
|
||||||
</SpinnerErrorButton>
|
<SpinnerErrorButton
|
||||||
</ModalFooter>
|
isSpinning={isSaving}
|
||||||
</ModalContent>
|
error={saveError}
|
||||||
);
|
onPress={onSavePress}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</ModalFooter>
|
||||||
|
</Measure>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EditQualityProfileModalContent.propTypes = {
|
EditQualityProfileModalContent.propTypes = {
|
||||||
|
editGroups: PropTypes.bool.isRequired,
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
|
@ -142,6 +244,7 @@ EditQualityProfileModalContent.propTypes = {
|
||||||
onInputChange: PropTypes.func.isRequired,
|
onInputChange: PropTypes.func.isRequired,
|
||||||
onCutoffChange: PropTypes.func.isRequired,
|
onCutoffChange: PropTypes.func.isRequired,
|
||||||
onSavePress: PropTypes.func.isRequired,
|
onSavePress: PropTypes.func.isRequired,
|
||||||
|
onContentHeightChange: PropTypes.func.isRequired,
|
||||||
onModalClose: PropTypes.func.isRequired,
|
onModalClose: PropTypes.func.isRequired,
|
||||||
onDeleteQualityProfilePress: PropTypes.func
|
onDeleteQualityProfilePress: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,6 +8,29 @@ import { fetchQualityProfileSchema, setQualityProfileValue, saveQualityProfile }
|
||||||
import connectSection from 'Store/connectSection';
|
import connectSection from 'Store/connectSection';
|
||||||
import EditQualityProfileModalContent from './EditQualityProfileModalContent';
|
import EditQualityProfileModalContent from './EditQualityProfileModalContent';
|
||||||
|
|
||||||
|
function getQualityItemGroupId(qualityProfile) {
|
||||||
|
// Get items with an `id` and filter out null/undefined values
|
||||||
|
const ids = _.filter(_.map(qualityProfile.items.value, 'id'), (i) => i != null);
|
||||||
|
|
||||||
|
return Math.max(1000, ...ids) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseIndex(index) {
|
||||||
|
const split = index.split('.');
|
||||||
|
|
||||||
|
if (split.length === 1) {
|
||||||
|
return [
|
||||||
|
null,
|
||||||
|
parseInt(split[0]) - 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
parseInt(split[0]) - 1,
|
||||||
|
parseInt(split[1]) - 1
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function createQualitiesSelector() {
|
function createQualitiesSelector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
createProviderSettingsSelector(),
|
createProviderSettingsSelector(),
|
||||||
|
@ -17,12 +40,19 @@ function createQualitiesSelector() {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return _.reduceRight(items.value, (result, { allowed, quality }) => {
|
return _.reduceRight(items.value, (result, { allowed, id, name, quality }) => {
|
||||||
if (allowed) {
|
if (allowed) {
|
||||||
result.push({
|
if (id) {
|
||||||
key: quality.id,
|
result.push({
|
||||||
value: quality.name
|
key: id,
|
||||||
});
|
value: name
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result.push({
|
||||||
|
key: quality.id,
|
||||||
|
value: quality.name
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -61,8 +91,10 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
dragIndex: null,
|
dragQualityIndex: null,
|
||||||
dropIndex: null
|
dropQualityIndex: null,
|
||||||
|
dropPosition: null,
|
||||||
|
editGroups: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +110,33 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Control
|
||||||
|
|
||||||
|
ensureCutoff = (qualityProfile) => {
|
||||||
|
const cutoff = qualityProfile.cutoff.value;
|
||||||
|
|
||||||
|
const cutoffItem = _.find(qualityProfile.items.value, (i) => {
|
||||||
|
if (!cutoff) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return i.id === cutoff || (i.quality && i.quality.id === cutoff);
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
|
||||||
|
if (!cutoff || !cutoffItem || !cutoffItem.allowed) {
|
||||||
|
const firstAllowed = _.find(qualityProfile.items.value, { allowed: true });
|
||||||
|
let cutoffId = null;
|
||||||
|
|
||||||
|
if (firstAllowed) {
|
||||||
|
cutoffId = firstAllowed.quality ? firstAllowed.quality.id : firstAllowed.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({ name: 'cutoff', value: cutoffId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
|
@ -87,9 +146,17 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
|
|
||||||
onCutoffChange = ({ name, value }) => {
|
onCutoffChange = ({ name, value }) => {
|
||||||
const id = parseInt(value);
|
const id = parseInt(value);
|
||||||
const item = _.find(this.props.item.items.value, (i) => i.quality.id === id);
|
const item = _.find(this.props.item.items.value, (i) => {
|
||||||
|
if (i.quality) {
|
||||||
|
return i.quality.id === id;
|
||||||
|
}
|
||||||
|
|
||||||
this.props.setQualityProfileValue({ name, value: item.quality });
|
return i.id === id;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cutoffId = item.quality ? item.quality.id : item.id;
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({ name, value: cutoffId });
|
||||||
}
|
}
|
||||||
|
|
||||||
onSavePress = () => {
|
onSavePress = () => {
|
||||||
|
@ -98,58 +165,239 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
|
|
||||||
onQualityProfileItemAllowedChange = (id, allowed) => {
|
onQualityProfileItemAllowedChange = (id, allowed) => {
|
||||||
const qualityProfile = _.cloneDeep(this.props.item);
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
const items = qualityProfile.items.value;
|
||||||
|
const item = _.find(qualityProfile.items.value, (i) => i.quality && i.quality.id === id);
|
||||||
|
|
||||||
const item = _.find(qualityProfile.items.value, (i) => i.quality.id === id);
|
|
||||||
item.allowed = allowed;
|
item.allowed = allowed;
|
||||||
|
|
||||||
this.props.setQualityProfileValue({
|
this.props.setQualityProfileValue({
|
||||||
name: 'items',
|
name: 'items',
|
||||||
value: qualityProfile.items.value
|
value: items
|
||||||
});
|
});
|
||||||
|
|
||||||
const cutoff = qualityProfile.cutoff.value;
|
this.ensureCutoff(qualityProfile);
|
||||||
|
|
||||||
// If the cutoff isn't allowed anymore or there isn't a cutoff set one
|
|
||||||
if (!cutoff || !_.find(qualityProfile.items.value, (i) => i.quality.id === cutoff.id).allowed) {
|
|
||||||
const firstAllowed = _.find(qualityProfile.items.value, { allowed: true });
|
|
||||||
|
|
||||||
this.props.setQualityProfileValue({ name: 'cutoff', value: firstAllowed ? firstAllowed.quality : null });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onQualityProfileItemDragMove = (dragIndex, dropIndex) => {
|
onItemGroupAllowedChange = (id, allowed) => {
|
||||||
if (this.state.dragIndex !== dragIndex || this.state.dropIndex !== dropIndex) {
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
const items = qualityProfile.items.value;
|
||||||
|
const item = _.find(qualityProfile.items.value, (i) => i.id === id);
|
||||||
|
|
||||||
|
item.allowed = allowed;
|
||||||
|
|
||||||
|
// Update each item in the group (for consistency only)
|
||||||
|
item.items.forEach((i) => {
|
||||||
|
i.allowed = allowed;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: items
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ensureCutoff(qualityProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
onItemGroupNameChange = (id, name) => {
|
||||||
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
const items = qualityProfile.items.value;
|
||||||
|
const group = _.find(items, (i) => i.id === id);
|
||||||
|
|
||||||
|
group.name = name;
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: items
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onCreateGroupPress = (id) => {
|
||||||
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
const items = qualityProfile.items.value;
|
||||||
|
const item = _.find(items, (i) => i.quality && i.quality.id === id);
|
||||||
|
const index = items.indexOf(item);
|
||||||
|
const groupId = getQualityItemGroupId(qualityProfile);
|
||||||
|
|
||||||
|
const group = {
|
||||||
|
id: groupId,
|
||||||
|
name: item.quality.name,
|
||||||
|
allowed: item.allowed,
|
||||||
|
items: [
|
||||||
|
item
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the group in the same location the quality item was in.
|
||||||
|
items.splice(index, 1, group);
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: items
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ensureCutoff(qualityProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteGroupPress = (id) => {
|
||||||
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
const items = qualityProfile.items.value;
|
||||||
|
const group = _.find(items, (i) => i.id === id);
|
||||||
|
const index = items.indexOf(group);
|
||||||
|
|
||||||
|
// Add the items in the same location the group was in
|
||||||
|
items.splice(index, 1, ...group.items);
|
||||||
|
|
||||||
|
this.props.setQualityProfileValue({
|
||||||
|
name: 'items',
|
||||||
|
value: items
|
||||||
|
});
|
||||||
|
|
||||||
|
this.ensureCutoff(qualityProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
onQualityProfileItemDragMove = (options) => {
|
||||||
|
const {
|
||||||
|
dragQualityIndex,
|
||||||
|
dropQualityIndex,
|
||||||
|
dropPosition
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
|
||||||
|
const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(dropPosition === 'below' && dropItemIndex - 1 === dragItemIndex) ||
|
||||||
|
(dropPosition === 'above' && dropItemIndex + 1 === dragItemIndex)
|
||||||
|
) {
|
||||||
|
if (
|
||||||
|
this.state.dragQualityIndex != null &&
|
||||||
|
this.state.dropQualityIndex != null &&
|
||||||
|
this.state.dropPosition != null
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
dragQualityIndex: null,
|
||||||
|
dropQualityIndex: null,
|
||||||
|
dropPosition: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjustedDropQualityIndex = dropQualityIndex;
|
||||||
|
|
||||||
|
// Correct dragging out of a group to the position above
|
||||||
|
if (
|
||||||
|
dropPosition === 'above' &&
|
||||||
|
dragGroupIndex !== dropGroupIndex &&
|
||||||
|
dropGroupIndex != null
|
||||||
|
) {
|
||||||
|
// Add 1 to the group index and 2 to the item index so it's inserted above in the correct group
|
||||||
|
adjustedDropQualityIndex = `${dropGroupIndex + 1}.${dropItemIndex + 2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct inserting above outside a group
|
||||||
|
if (
|
||||||
|
dropPosition === 'above' &&
|
||||||
|
dragGroupIndex !== dropGroupIndex &&
|
||||||
|
dropGroupIndex == null
|
||||||
|
) {
|
||||||
|
// Add 2 to the item index so it's entered in the correct place
|
||||||
|
adjustedDropQualityIndex = `${dropItemIndex + 2}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct inserting below a quality within the same group (when moving a lower item)
|
||||||
|
if (
|
||||||
|
dropPosition === 'below' &&
|
||||||
|
dragGroupIndex === dropGroupIndex &&
|
||||||
|
dropGroupIndex != null &&
|
||||||
|
dragItemIndex < dropItemIndex
|
||||||
|
) {
|
||||||
|
// Add 1 to the group index leave the item index
|
||||||
|
adjustedDropQualityIndex = `${dropGroupIndex + 1}.${dropItemIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct inserting below a quality outside a group (when moving a lower item)
|
||||||
|
if (
|
||||||
|
dropPosition === 'below' &&
|
||||||
|
dragGroupIndex === dropGroupIndex &&
|
||||||
|
dropGroupIndex == null &&
|
||||||
|
dragItemIndex < dropItemIndex
|
||||||
|
) {
|
||||||
|
// Leave the item index so it's inserted below the item
|
||||||
|
adjustedDropQualityIndex = `${dropItemIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dragQualityIndex !== this.state.dragQualityIndex ||
|
||||||
|
adjustedDropQualityIndex !== this.state.dropQualityIndex ||
|
||||||
|
dropPosition !== this.state.dropPosition
|
||||||
|
) {
|
||||||
this.setState({
|
this.setState({
|
||||||
dragIndex,
|
dragQualityIndex,
|
||||||
dropIndex
|
dropQualityIndex: adjustedDropQualityIndex,
|
||||||
|
dropPosition
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onQualityProfileItemDragEnd = ({ id }, didDrop) => {
|
onQualityProfileItemDragEnd = (didDrop) => {
|
||||||
const {
|
const {
|
||||||
dragIndex,
|
dragQualityIndex,
|
||||||
dropIndex
|
dropQualityIndex
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (didDrop && dropIndex !== null) {
|
if (didDrop && dropQualityIndex != null) {
|
||||||
const qualityProfile = _.cloneDeep(this.props.item);
|
const qualityProfile = _.cloneDeep(this.props.item);
|
||||||
|
const items = qualityProfile.items.value;
|
||||||
|
const [dragGroupIndex, dragItemIndex] = parseIndex(dragQualityIndex);
|
||||||
|
const [dropGroupIndex, dropItemIndex] = parseIndex(dropQualityIndex);
|
||||||
|
|
||||||
const items = qualityProfile.items.value.splice(dragIndex, 1);
|
let item = null;
|
||||||
qualityProfile.items.value.splice(dropIndex, 0, items[0]);
|
let dropGroup = null;
|
||||||
|
|
||||||
|
// Get the group before moving anything so we know the correct place to drop it.
|
||||||
|
if (dropGroupIndex != null) {
|
||||||
|
dropGroup = items[dropGroupIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragGroupIndex == null) {
|
||||||
|
item = items.splice(dragItemIndex, 1)[0];
|
||||||
|
} else {
|
||||||
|
const group = items[dragGroupIndex];
|
||||||
|
item = group.items.splice(dragItemIndex, 1)[0];
|
||||||
|
|
||||||
|
// If the group is now empty, destroy it.
|
||||||
|
if (!group.items.length) {
|
||||||
|
items.splice(dragGroupIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropGroupIndex == null) {
|
||||||
|
items.splice(dropItemIndex, 0, item);
|
||||||
|
} else {
|
||||||
|
dropGroup.items.splice(dropItemIndex, 0, item);
|
||||||
|
}
|
||||||
|
|
||||||
this.props.setQualityProfileValue({
|
this.props.setQualityProfileValue({
|
||||||
name: 'items',
|
name: 'items',
|
||||||
value: qualityProfile.items.value
|
value: items
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.ensureCutoff(qualityProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
dragIndex: null,
|
dragQualityIndex: null,
|
||||||
dropIndex: null
|
dropQualityIndex: null,
|
||||||
|
dropPosition: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onToggleEditGroupsMode = () => {
|
||||||
|
this.setState({ editGroups: !this.state.editGroups });
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
|
@ -165,9 +413,14 @@ class EditQualityProfileModalContentConnector extends Component {
|
||||||
onSavePress={this.onSavePress}
|
onSavePress={this.onSavePress}
|
||||||
onInputChange={this.onInputChange}
|
onInputChange={this.onInputChange}
|
||||||
onCutoffChange={this.onCutoffChange}
|
onCutoffChange={this.onCutoffChange}
|
||||||
|
onCreateGroupPress={this.onCreateGroupPress}
|
||||||
|
onDeleteGroupPress={this.onDeleteGroupPress}
|
||||||
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
|
onQualityProfileItemAllowedChange={this.onQualityProfileItemAllowedChange}
|
||||||
|
onItemGroupAllowedChange={this.onItemGroupAllowedChange}
|
||||||
|
onItemGroupNameChange={this.onItemGroupNameChange}
|
||||||
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
onQualityProfileItemDragMove={this.onQualityProfileItemDragMove}
|
||||||
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
onQualityProfileItemDragEnd={this.onQualityProfileItemDragEnd}
|
||||||
|
onToggleEditGroupsMode={this.onToggleEditGroupsMode}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,3 +17,10 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltipLabel {
|
||||||
|
composes: label from 'Components/Label.css';
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
|
@ -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 { kinds } from 'Helpers/Props';
|
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import Card from 'Components/Card';
|
import Card from 'Components/Card';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||||
|
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||||
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
|
import EditQualityProfileModalConnector from './EditQualityProfileModalConnector';
|
||||||
import styles from './QualityProfile.css';
|
import styles from './QualityProfile.css';
|
||||||
|
|
||||||
|
@ -75,16 +76,54 @@ class QualityProfile extends Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isCutoff = item.quality.id === cutoff.id;
|
if (item.quality) {
|
||||||
|
const isCutoff = item.quality.id === cutoff;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={item.quality.id}
|
||||||
|
kind={isCutoff ? kinds.INFO : kinds.default}
|
||||||
|
title={isCutoff ? 'Cutoff' : null}
|
||||||
|
>
|
||||||
|
{item.quality.name}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCutoff = item.id === cutoff;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Label
|
<Tooltip
|
||||||
key={item.quality.id}
|
key={item.id}
|
||||||
kind={isCutoff ? kinds.INFO : kinds.default}
|
className={styles.tooltipLabel}
|
||||||
title={isCutoff ? 'Cutoff' : null}
|
anchor={
|
||||||
>
|
<Label
|
||||||
{item.quality.name}
|
kind={isCutoff ? kinds.INFO : kinds.default}
|
||||||
</Label>
|
title={isCutoff ? 'Cutoff' : null}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Label>
|
||||||
|
}
|
||||||
|
tooltip={
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
item.items.map((groupItem) => {
|
||||||
|
return (
|
||||||
|
<Label
|
||||||
|
key={groupItem.quality.id}
|
||||||
|
kind={isCutoff ? kinds.INFO : kinds.default}
|
||||||
|
title={isCutoff ? 'Cutoff' : null}
|
||||||
|
>
|
||||||
|
{groupItem.quality.name}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
kind={kinds.INVERSE}
|
||||||
|
position={tooltipPositions.TOP}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -115,7 +154,7 @@ class QualityProfile extends Component {
|
||||||
QualityProfile.propTypes = {
|
QualityProfile.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
cutoff: PropTypes.object.isRequired,
|
cutoff: PropTypes.number.isRequired,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
onConfirmDeleteQualityProfile: PropTypes.func.isRequired
|
onConfirmDeleteQualityProfile: PropTypes.func.isRequired
|
||||||
|
|
|
@ -5,25 +5,56 @@
|
||||||
border: 1px solid #aaa;
|
border: 1px solid #aaa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: #fafafa;
|
background: #fafafa;
|
||||||
|
|
||||||
|
&.isInGroup {
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkContainer {
|
.checkInputContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
margin-bottom: 7px;
|
margin-bottom: 5px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qualityName {
|
.checkInput {
|
||||||
|
composes: input from 'Components/Form/CheckInput.css';
|
||||||
|
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityNameContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
line-height: 36px;
|
line-height: $qualityProfileItemHeight;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.qualityName {
|
||||||
|
&.isInGroup {
|
||||||
|
margin-left: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.notAllowed {
|
||||||
|
color: #c6c6c6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.createGroupButton {
|
||||||
|
composes: buton from 'Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-left: 8px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.dragHandle {
|
.dragHandle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -42,3 +73,13 @@
|
||||||
.isDragging {
|
.isDragging {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.isPreview {
|
||||||
|
.qualityName {
|
||||||
|
margin-left: 14px;
|
||||||
|
|
||||||
|
&.isInGroup {
|
||||||
|
margin-left: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
import CheckInput from 'Components/Form/CheckInput';
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
import styles from './QualityProfileItem.css';
|
import styles from './QualityProfileItem.css';
|
||||||
|
|
||||||
|
@ -20,14 +21,27 @@ class QualityProfileItem extends Component {
|
||||||
onQualityProfileItemAllowedChange(qualityId, value);
|
onQualityProfileItemAllowedChange(qualityId, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onCreateGroupPress = () => {
|
||||||
|
const {
|
||||||
|
qualityId,
|
||||||
|
onCreateGroupPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onCreateGroupPress(qualityId);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
editGroups,
|
||||||
|
isPreview,
|
||||||
|
groupId,
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed,
|
||||||
isDragging,
|
isDragging,
|
||||||
|
isOverCurrent,
|
||||||
connectDragSource
|
connectDragSource
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
|
@ -36,18 +50,44 @@ class QualityProfileItem extends Component {
|
||||||
className={classNames(
|
className={classNames(
|
||||||
styles.qualityProfileItem,
|
styles.qualityProfileItem,
|
||||||
isDragging && styles.isDragging,
|
isDragging && styles.isDragging,
|
||||||
|
isPreview && styles.isPreview,
|
||||||
|
isOverCurrent && styles.isOverCurrent,
|
||||||
|
groupId && styles.isInGroup
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
className={styles.qualityName}
|
className={styles.qualityNameContainer}
|
||||||
>
|
>
|
||||||
<CheckInput
|
{
|
||||||
containerClassName={styles.checkContainer}
|
editGroups && !groupId && !isPreview &&
|
||||||
name={name}
|
<IconButton
|
||||||
value={allowed}
|
className={styles.createGroupButton}
|
||||||
onChange={this.onAllowedChange}
|
name={icons.GROUP}
|
||||||
/>
|
title="Group"
|
||||||
{name}
|
onPress={this.onCreateGroupPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!editGroups &&
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
containerClassName={styles.checkInputContainer}
|
||||||
|
name={name}
|
||||||
|
value={allowed}
|
||||||
|
isDisabled={!!groupId}
|
||||||
|
onChange={this.onAllowedChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div className={classNames(
|
||||||
|
styles.qualityName,
|
||||||
|
groupId && styles.isInGroup,
|
||||||
|
!allowed && styles.notAllowed
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{
|
{
|
||||||
|
@ -55,6 +95,7 @@ class QualityProfileItem extends Component {
|
||||||
<div className={styles.dragHandle}>
|
<div className={styles.dragHandle}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.dragIcon}
|
className={styles.dragIcon}
|
||||||
|
title="Create group"
|
||||||
name={icons.REORDER}
|
name={icons.REORDER}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -66,16 +107,23 @@ class QualityProfileItem extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileItem.propTypes = {
|
QualityProfileItem.propTypes = {
|
||||||
|
editGroups: PropTypes.bool,
|
||||||
|
isPreview: PropTypes.bool,
|
||||||
|
groupId: PropTypes.number,
|
||||||
qualityId: PropTypes.number.isRequired,
|
qualityId: PropTypes.number.isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
allowed: PropTypes.bool.isRequired,
|
allowed: PropTypes.bool.isRequired,
|
||||||
sortIndex: PropTypes.number.isRequired,
|
|
||||||
isDragging: PropTypes.bool.isRequired,
|
isDragging: PropTypes.bool.isRequired,
|
||||||
|
isOverCurrent: PropTypes.bool.isRequired,
|
||||||
|
isInGroup: PropTypes.bool,
|
||||||
connectDragSource: PropTypes.func,
|
connectDragSource: PropTypes.func,
|
||||||
|
onCreateGroupPress: PropTypes.func,
|
||||||
onQualityProfileItemAllowedChange: PropTypes.func
|
onQualityProfileItemAllowedChange: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
QualityProfileItem.defaultProps = {
|
QualityProfileItem.defaultProps = {
|
||||||
|
isPreview: false,
|
||||||
|
isOverCurrent: false,
|
||||||
// The drag preview will not connect the drag handle.
|
// The drag preview will not connect the drag handle.
|
||||||
connectDragSource: (node) => node
|
connectDragSource: (node) => node
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,8 @@ import DragPreviewLayer from 'Components/DragPreviewLayer';
|
||||||
import QualityProfileItem from './QualityProfileItem';
|
import QualityProfileItem from './QualityProfileItem';
|
||||||
import styles from './QualityProfileItemDragPreview.css';
|
import styles from './QualityProfileItemDragPreview.css';
|
||||||
|
|
||||||
const formGroupSmallWidth = parseInt(dimensions.formGroupSmallWidth);
|
const formGroupExtraSmallWidth = parseInt(dimensions.formGroupExtraSmallWidth);
|
||||||
const formLabelWidth = parseInt(dimensions.formLabelWidth);
|
const formLabelSmallWidth = parseInt(dimensions.formLabelSmallWidth);
|
||||||
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
const formLabelRightMarginWidth = parseInt(dimensions.formLabelRightMarginWidth);
|
||||||
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
const dragHandleWidth = parseInt(dimensions.dragHandleWidth);
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class QualityProfileItemDragPreview extends Component {
|
||||||
// list item and the preview is wider than the drag handle.
|
// list item and the preview is wider than the drag handle.
|
||||||
|
|
||||||
const { x, y } = currentOffset;
|
const { x, y } = currentOffset;
|
||||||
const handleOffset = formGroupSmallWidth - formLabelWidth - formLabelRightMarginWidth - dragHandleWidth;
|
const handleOffset = formGroupExtraSmallWidth - formLabelSmallWidth - formLabelRightMarginWidth - dragHandleWidth;
|
||||||
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
const transform = `translate3d(${x - handleOffset}px, ${y}px, 0)`;
|
||||||
|
|
||||||
const style = {
|
const style = {
|
||||||
|
@ -51,12 +51,15 @@ class QualityProfileItemDragPreview extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
editGroups,
|
||||||
|
groupId,
|
||||||
qualityId,
|
qualityId,
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed
|
||||||
sortIndex
|
|
||||||
} = item;
|
} = item;
|
||||||
|
|
||||||
|
// TODO: Show a different preview for groups
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragPreviewLayer>
|
<DragPreviewLayer>
|
||||||
<div
|
<div
|
||||||
|
@ -64,10 +67,11 @@ class QualityProfileItemDragPreview extends Component {
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<QualityProfileItem
|
<QualityProfileItem
|
||||||
qualityId={qualityId}
|
editGroups={editGroups}
|
||||||
|
isPreview={true}
|
||||||
|
qualityId={groupId || qualityId}
|
||||||
name={name}
|
name={name}
|
||||||
allowed={allowed}
|
allowed={allowed}
|
||||||
sortIndex={sortIndex}
|
|
||||||
isDragging={false}
|
isDragging={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
.qualityProfileItemDragSource {
|
.qualityProfileItemDragSource {
|
||||||
padding: 4px 0;
|
padding: $qualityProfileItemDragSourcePadding 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qualityProfileItemPlaceholder {
|
.qualityProfileItemPlaceholder {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 36px;
|
height: $qualityProfileItemHeight;
|
||||||
border: 1px dotted #aaa;
|
border: 1px dotted #aaa;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,44 +5,86 @@ import { DragSource, DropTarget } from 'react-dnd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes';
|
import { QUALITY_PROFILE_ITEM } from 'Helpers/dragTypes';
|
||||||
import QualityProfileItem from './QualityProfileItem';
|
import QualityProfileItem from './QualityProfileItem';
|
||||||
|
import QualityProfileItemGroup from './QualityProfileItemGroup';
|
||||||
import styles from './QualityProfileItemDragSource.css';
|
import styles from './QualityProfileItemDragSource.css';
|
||||||
|
|
||||||
const qualityProfileItemDragSource = {
|
const qualityProfileItemDragSource = {
|
||||||
beginDrag({ qualityId, name, allowed, sortIndex }) {
|
beginDrag(props) {
|
||||||
return {
|
const {
|
||||||
|
editGroups,
|
||||||
|
qualityIndex,
|
||||||
|
groupId,
|
||||||
qualityId,
|
qualityId,
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed
|
||||||
sortIndex
|
} = props;
|
||||||
|
|
||||||
|
return {
|
||||||
|
editGroups,
|
||||||
|
qualityIndex,
|
||||||
|
groupId,
|
||||||
|
qualityId,
|
||||||
|
isGroup: !qualityId,
|
||||||
|
name,
|
||||||
|
allowed
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
endDrag(props, monitor, component) {
|
endDrag(props, monitor, component) {
|
||||||
props.onQualityProfileItemDragEnd(monitor.getItem(), monitor.didDrop());
|
props.onQualityProfileItemDragEnd(monitor.didDrop());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const qualityProfileItemDropTarget = {
|
const qualityProfileItemDropTarget = {
|
||||||
hover(props, monitor, component) {
|
hover(props, monitor, component) {
|
||||||
const dragIndex = monitor.getItem().sortIndex;
|
const {
|
||||||
const hoverIndex = props.sortIndex;
|
qualityIndex: dragQualityIndex,
|
||||||
|
isGroup: isDragGroup
|
||||||
|
} = monitor.getItem();
|
||||||
|
|
||||||
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
|
const dropQualityIndex = props.qualityIndex;
|
||||||
|
const isDropGroupItem = !!(props.qualityId && props.groupId);
|
||||||
|
|
||||||
|
// Use childNodeIndex to select the correct node to get the middle of so
|
||||||
|
// we don't bounce between above and below causing rapid setState calls.
|
||||||
|
const childNodeIndex = component.props.isOverCurrent && component.props.isDraggingUp ? 1 :0;
|
||||||
|
const componentDOMNode = findDOMNode(component).children[childNodeIndex];
|
||||||
|
const hoverBoundingRect = componentDOMNode.getBoundingClientRect();
|
||||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||||
const clientOffset = monitor.getClientOffset();
|
const clientOffset = monitor.getClientOffset();
|
||||||
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
|
||||||
|
|
||||||
// Moving up, only trigger if drag position is above 50%
|
// If we're hovering over a child don't trigger on the parent
|
||||||
if (dragIndex < hoverIndex && hoverClientY > hoverMiddleY) {
|
if (!monitor.isOver({ shallow: true })) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Moving down, only trigger if drag position is below 50%
|
// Don't show targets for dropping on self
|
||||||
if (dragIndex > hoverIndex && hoverClientY < hoverMiddleY) {
|
if (dragQualityIndex === dropQualityIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
props.onQualityProfileItemDragMove(dragIndex, hoverIndex);
|
// Don't allow a group to be dropped inside a group
|
||||||
|
if (isDragGroup && isDropGroupItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dropPosition = null;
|
||||||
|
|
||||||
|
// Determine drop position based on position over target
|
||||||
|
if (hoverClientY > hoverMiddleY) {
|
||||||
|
dropPosition = 'below';
|
||||||
|
} else if (hoverClientY < hoverMiddleY) {
|
||||||
|
dropPosition = 'above';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onQualityProfileItemDragMove({
|
||||||
|
dragQualityIndex,
|
||||||
|
dropQualityIndex,
|
||||||
|
dropPosition
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -56,7 +98,8 @@ function collectDragSource(connect, monitor) {
|
||||||
function collectDropTarget(connect, monitor) {
|
function collectDropTarget(connect, monitor) {
|
||||||
return {
|
return {
|
||||||
connectDropTarget: connect.dropTarget(),
|
connectDropTarget: connect.dropTarget(),
|
||||||
isOver: monitor.isOver()
|
isOver: monitor.isOver(),
|
||||||
|
isOverCurrent: monitor.isOver({ shallow: true })
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,25 +110,30 @@ class QualityProfileItemDragSource extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
editGroups,
|
||||||
|
groupId,
|
||||||
qualityId,
|
qualityId,
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed,
|
||||||
sortIndex,
|
items,
|
||||||
|
qualityIndex,
|
||||||
isDragging,
|
isDragging,
|
||||||
isDraggingUp,
|
isDraggingUp,
|
||||||
isDraggingDown,
|
isDraggingDown,
|
||||||
isOver,
|
isOverCurrent,
|
||||||
connectDragSource,
|
connectDragSource,
|
||||||
connectDropTarget,
|
connectDropTarget,
|
||||||
onQualityProfileItemAllowedChange
|
onCreateGroupPress,
|
||||||
|
onDeleteGroupPress,
|
||||||
|
onQualityProfileItemAllowedChange,
|
||||||
|
onItemGroupAllowedChange,
|
||||||
|
onItemGroupNameChange,
|
||||||
|
onQualityProfileItemDragMove,
|
||||||
|
onQualityProfileItemDragEnd
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isBefore = !isDragging && isDraggingUp && isOver;
|
const isBefore = !isDragging && isDraggingUp && isOverCurrent;
|
||||||
const isAfter = !isDragging && isDraggingDown && isOver;
|
const isAfter = !isDragging && isDraggingDown && isOverCurrent;
|
||||||
|
|
||||||
// if (isDragging && !isOver) {
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
|
|
||||||
return connectDropTarget(
|
return connectDropTarget(
|
||||||
<div
|
<div
|
||||||
|
@ -105,16 +153,44 @@ class QualityProfileItemDragSource extends Component {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
|
||||||
<QualityProfileItem
|
{
|
||||||
qualityId={qualityId}
|
!!groupId && qualityId == null &&
|
||||||
name={name}
|
<QualityProfileItemGroup
|
||||||
allowed={allowed}
|
editGroups={editGroups}
|
||||||
sortIndex={sortIndex}
|
groupId={groupId}
|
||||||
isDragging={isDragging}
|
name={name}
|
||||||
isOver={isOver}
|
allowed={allowed}
|
||||||
connectDragSource={connectDragSource}
|
items={items}
|
||||||
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
qualityIndex={qualityIndex}
|
||||||
/>
|
isDragging={isDragging}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
connectDragSource={connectDragSource}
|
||||||
|
onDeleteGroupPress={onDeleteGroupPress}
|
||||||
|
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||||
|
onItemGroupAllowedChange={onItemGroupAllowedChange}
|
||||||
|
onItemGroupNameChange={onItemGroupNameChange}
|
||||||
|
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
||||||
|
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
qualityId != null &&
|
||||||
|
<QualityProfileItem
|
||||||
|
editGroups={editGroups}
|
||||||
|
groupId={groupId}
|
||||||
|
qualityId={qualityId}
|
||||||
|
name={name}
|
||||||
|
allowed={allowed}
|
||||||
|
qualityIndex={qualityIndex}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isOverCurrent={isOverCurrent}
|
||||||
|
connectDragSource={connectDragSource}
|
||||||
|
onCreateGroupPress={onCreateGroupPress}
|
||||||
|
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isAfter &&
|
isAfter &&
|
||||||
|
@ -131,17 +207,25 @@ class QualityProfileItemDragSource extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileItemDragSource.propTypes = {
|
QualityProfileItemDragSource.propTypes = {
|
||||||
qualityId: PropTypes.number.isRequired,
|
editGroups: PropTypes.bool.isRequired,
|
||||||
|
groupId: PropTypes.number,
|
||||||
|
qualityId: PropTypes.number,
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
allowed: PropTypes.bool.isRequired,
|
allowed: PropTypes.bool.isRequired,
|
||||||
sortIndex: PropTypes.number.isRequired,
|
items: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
qualityIndex: PropTypes.string.isRequired,
|
||||||
isDragging: PropTypes.bool,
|
isDragging: PropTypes.bool,
|
||||||
isDraggingUp: PropTypes.bool,
|
isDraggingUp: PropTypes.bool,
|
||||||
isDraggingDown: PropTypes.bool,
|
isDraggingDown: PropTypes.bool,
|
||||||
isOver: PropTypes.bool,
|
isOverCurrent: PropTypes.bool,
|
||||||
|
isInGroup: PropTypes.bool,
|
||||||
connectDragSource: PropTypes.func,
|
connectDragSource: PropTypes.func,
|
||||||
connectDropTarget: PropTypes.func,
|
connectDropTarget: PropTypes.func,
|
||||||
|
onCreateGroupPress: PropTypes.func,
|
||||||
|
onDeleteGroupPress: PropTypes.func,
|
||||||
onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
|
onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
|
||||||
|
onItemGroupAllowedChange: PropTypes.func,
|
||||||
|
onItemGroupNameChange: PropTypes.func,
|
||||||
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
||||||
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
.qualityProfileItemGroup {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
&.editGroups {
|
||||||
|
background: #fcfcfc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityProfileItemGroupInfo {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkInputContainer {
|
||||||
|
composes: checkInputContainer from './QualityProfileItem.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkInput {
|
||||||
|
composes: checkInput from './QualityProfileItem.css';
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameInput {
|
||||||
|
composes: text from 'Components/Form/TextInput.css';
|
||||||
|
|
||||||
|
margin-top: 4px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nameContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.notAllowed {
|
||||||
|
color: #c6c6c6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupQualities {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 2px 0 2px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityNameContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
margin-left: 2px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qualityNameLabel {
|
||||||
|
composes: qualityNameContainer;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.deleteGroupButton {
|
||||||
|
composes: buton from 'Components/Link/IconButton.css';
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
margin-left: 8px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragHandle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
width: $dragHandleWidth;
|
||||||
|
text-align: center;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragIcon {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.isDragging {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items {
|
||||||
|
margin: 0 50px 0 35px;
|
||||||
|
}
|
|
@ -0,0 +1,200 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React, { Component } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { icons } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Label from 'Components/Label';
|
||||||
|
import IconButton from 'Components/Link/IconButton';
|
||||||
|
import CheckInput from 'Components/Form/CheckInput';
|
||||||
|
import TextInput from 'Components/Form/TextInput';
|
||||||
|
import QualityProfileItemDragSource from './QualityProfileItemDragSource';
|
||||||
|
import styles from './QualityProfileItemGroup.css';
|
||||||
|
|
||||||
|
class QualityProfileItemGroup extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onAllowedChange = ({ value }) => {
|
||||||
|
const {
|
||||||
|
groupId,
|
||||||
|
onItemGroupAllowedChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onItemGroupAllowedChange(groupId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onNameChange = ({ value }) => {
|
||||||
|
const {
|
||||||
|
groupId,
|
||||||
|
onItemGroupNameChange
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onItemGroupNameChange(groupId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onDeleteGroupPress = ({ value }) => {
|
||||||
|
const {
|
||||||
|
groupId,
|
||||||
|
onDeleteGroupPress
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
onDeleteGroupPress(groupId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Render
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const {
|
||||||
|
editGroups,
|
||||||
|
groupId,
|
||||||
|
name,
|
||||||
|
allowed,
|
||||||
|
items,
|
||||||
|
qualityIndex,
|
||||||
|
isDragging,
|
||||||
|
isDraggingUp,
|
||||||
|
isDraggingDown,
|
||||||
|
connectDragSource,
|
||||||
|
onQualityProfileItemAllowedChange,
|
||||||
|
onQualityProfileItemDragMove,
|
||||||
|
onQualityProfileItemDragEnd
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
styles.qualityProfileItemGroup,
|
||||||
|
editGroups && styles.editGroups,
|
||||||
|
isDragging && styles.isDragging,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className={styles.qualityProfileItemGroupInfo}>
|
||||||
|
{
|
||||||
|
editGroups &&
|
||||||
|
<div className={styles.qualityNameContainer}>
|
||||||
|
<IconButton
|
||||||
|
className={styles.deleteGroupButton}
|
||||||
|
name={icons.UNGROUP}
|
||||||
|
title="Ungroup"
|
||||||
|
onPress={this.onDeleteGroupPress}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
className={styles.nameInput}
|
||||||
|
name="name"
|
||||||
|
value={name}
|
||||||
|
onChange={this.onNameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
!editGroups &&
|
||||||
|
<label
|
||||||
|
className={styles.qualityNameLabel}
|
||||||
|
>
|
||||||
|
<CheckInput
|
||||||
|
className={styles.checkInput}
|
||||||
|
containerClassName={styles.checkInputContainer}
|
||||||
|
name="allowed"
|
||||||
|
value={allowed}
|
||||||
|
onChange={this.onAllowedChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.nameContainer}>
|
||||||
|
<div className={classNames(
|
||||||
|
styles.name,
|
||||||
|
!allowed && styles.notAllowed
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.groupQualities}>
|
||||||
|
{
|
||||||
|
items.map(({ quality }) => {
|
||||||
|
return (
|
||||||
|
<Label key={quality.id}>
|
||||||
|
{quality.name}
|
||||||
|
</Label>
|
||||||
|
);
|
||||||
|
}).reverse()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
connectDragSource(
|
||||||
|
<div className={styles.dragHandle}>
|
||||||
|
<Icon
|
||||||
|
className={styles.dragIcon}
|
||||||
|
name={icons.REORDER}
|
||||||
|
title="Reorder"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
editGroups &&
|
||||||
|
<div className={styles.items}>
|
||||||
|
{
|
||||||
|
items.map(({ quality }, index) => {
|
||||||
|
return (
|
||||||
|
<QualityProfileItemDragSource
|
||||||
|
key={quality.id}
|
||||||
|
editGroups={editGroups}
|
||||||
|
groupId={groupId}
|
||||||
|
qualityId={quality.id}
|
||||||
|
name={quality.name}
|
||||||
|
allowed={allowed}
|
||||||
|
items={items}
|
||||||
|
qualityIndex={`${qualityIndex}.${index + 1}`}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
isInGroup={true}
|
||||||
|
onQualityProfileItemAllowedChange={onQualityProfileItemAllowedChange}
|
||||||
|
onQualityProfileItemDragMove={onQualityProfileItemDragMove}
|
||||||
|
onQualityProfileItemDragEnd={onQualityProfileItemDragEnd}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}).reverse()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QualityProfileItemGroup.propTypes = {
|
||||||
|
editGroups: PropTypes.bool,
|
||||||
|
groupId: PropTypes.number.isRequired,
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
allowed: PropTypes.bool.isRequired,
|
||||||
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
qualityIndex: PropTypes.string.isRequired,
|
||||||
|
isDragging: PropTypes.bool.isRequired,
|
||||||
|
isDraggingUp: PropTypes.bool.isRequired,
|
||||||
|
isDraggingDown: PropTypes.bool.isRequired,
|
||||||
|
connectDragSource: PropTypes.func,
|
||||||
|
onItemGroupAllowedChange: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileItemAllowedChange: PropTypes.func.isRequired,
|
||||||
|
onItemGroupNameChange: PropTypes.func.isRequired,
|
||||||
|
onDeleteGroupPress: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileItemDragMove: PropTypes.func.isRequired,
|
||||||
|
onQualityProfileItemDragEnd: PropTypes.func.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
QualityProfileItemGroup.defaultProps = {
|
||||||
|
// The drag preview will not connect the drag handle.
|
||||||
|
connectDragSource: (node) => node
|
||||||
|
};
|
||||||
|
|
||||||
|
export default QualityProfileItemGroup;
|
|
@ -1,6 +1,15 @@
|
||||||
|
.editGroupsButton {
|
||||||
|
composes: button from 'Components/Link/Button.css';
|
||||||
|
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editGroupsButtonIcon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.qualities {
|
.qualities {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
/* TODO: This should consider the number of qualities in the list */
|
transition: min-height 200ms;
|
||||||
min-height: 550px;
|
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
|
import Measure from 'react-measure';
|
||||||
|
import { icons, kinds, sizes } from 'Helpers/Props';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Button from 'Components/Link/Button';
|
||||||
import FormGroup from 'Components/Form/FormGroup';
|
import FormGroup from 'Components/Form/FormGroup';
|
||||||
import FormLabel from 'Components/Form/FormLabel';
|
import FormLabel from 'Components/Form/FormLabel';
|
||||||
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
import FormInputHelpText from 'Components/Form/FormInputHelpText';
|
||||||
|
@ -9,26 +13,69 @@ import styles from './QualityProfileItems.css';
|
||||||
|
|
||||||
class QualityProfileItems extends Component {
|
class QualityProfileItems extends Component {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Lifecycle
|
||||||
|
|
||||||
|
constructor(props, context) {
|
||||||
|
super(props, context);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
qualitiesHeight: 0,
|
||||||
|
qualitiesHeightEditGroups: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.onToggleEditGroupsMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Listeners
|
||||||
|
|
||||||
|
onMeasure = ({ height }) => {
|
||||||
|
if (this.props.editGroups) {
|
||||||
|
this.setState({
|
||||||
|
qualitiesHeightEditGroups: height
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.setState({ qualitiesHeight: height });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleEditGroupsMode = () => {
|
||||||
|
this.props.onToggleEditGroupsMode();
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Render
|
// Render
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
dragIndex,
|
editGroups,
|
||||||
dropIndex,
|
dropQualityIndex,
|
||||||
|
dropPosition,
|
||||||
qualityProfileItems,
|
qualityProfileItems,
|
||||||
errors,
|
errors,
|
||||||
warnings,
|
warnings,
|
||||||
...otherProps
|
...otherProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const isDragging = dropIndex !== null;
|
const {
|
||||||
const isDraggingUp = isDragging && dropIndex > dragIndex;
|
qualitiesHeight,
|
||||||
const isDraggingDown = isDragging && dropIndex < dragIndex;
|
qualitiesHeightEditGroups
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
const isDragging = dropQualityIndex !== null;
|
||||||
|
const isDraggingUp = isDragging && dropPosition === 'above';
|
||||||
|
const isDraggingDown = isDragging && dropPosition === 'below';
|
||||||
|
const minHeight = editGroups ? qualitiesHeightEditGroups : qualitiesHeight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormGroup>
|
<FormGroup size={sizes.EXTRA_SMALL}>
|
||||||
<FormLabel>Qualities</FormLabel>
|
<FormLabel size={sizes.SMALL}>
|
||||||
|
Qualities
|
||||||
|
</FormLabel>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<FormInputHelpText
|
<FormInputHelpText
|
||||||
text="Qualities higher in the list are more preferred. Only checked qualities are wanted"
|
text="Qualities higher in the list are more preferred. Only checked qualities are wanted"
|
||||||
|
@ -60,27 +107,59 @@ class QualityProfileItems extends Component {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
<div className={styles.qualities}>
|
<Button
|
||||||
{
|
className={styles.editGroupsButton}
|
||||||
qualityProfileItems.map(({ allowed, quality }, index) => {
|
kind={kinds.PRIMARY}
|
||||||
return (
|
onPress={this.onToggleEditGroupsMode}
|
||||||
<QualityProfileItemDragSource
|
>
|
||||||
key={quality.id}
|
<div>
|
||||||
qualityId={quality.id}
|
<Icon
|
||||||
name={quality.name}
|
className={styles.editGroupsButtonIcon}
|
||||||
allowed={allowed}
|
name={editGroups ? icons.REORDER : icons.GROUP}
|
||||||
sortIndex={index}
|
/>
|
||||||
isDragging={isDragging}
|
|
||||||
isDraggingUp={isDraggingUp}
|
|
||||||
isDraggingDown={isDraggingDown}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}).reverse()
|
|
||||||
}
|
|
||||||
|
|
||||||
<QualityProfileItemDragPreview />
|
{
|
||||||
</div>
|
editGroups ? 'Done Editing Groups' : 'Edit Groups'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Measure
|
||||||
|
whitelist={['height']}
|
||||||
|
includeMargin={false}
|
||||||
|
onMeasure={this.onMeasure}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.qualities}
|
||||||
|
style={{ minHeight: `${minHeight}px` }}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
qualityProfileItems.map(({ id, name, allowed, quality, items }, index) => {
|
||||||
|
const identifier = quality ? quality.id : id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QualityProfileItemDragSource
|
||||||
|
key={identifier}
|
||||||
|
editGroups={editGroups}
|
||||||
|
groupId={id}
|
||||||
|
qualityId={quality && quality.id}
|
||||||
|
name={quality ? quality.name : name}
|
||||||
|
allowed={allowed}
|
||||||
|
items={items}
|
||||||
|
qualityIndex={`${index + 1}`}
|
||||||
|
isInGroup={false}
|
||||||
|
isDragging={isDragging}
|
||||||
|
isDraggingUp={isDraggingUp}
|
||||||
|
isDraggingDown={isDraggingDown}
|
||||||
|
{...otherProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}).reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
<QualityProfileItemDragPreview />
|
||||||
|
</div>
|
||||||
|
</Measure>
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
);
|
);
|
||||||
|
@ -88,11 +167,14 @@ class QualityProfileItems extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfileItems.propTypes = {
|
QualityProfileItems.propTypes = {
|
||||||
dragIndex: PropTypes.number,
|
editGroups: PropTypes.bool.isRequired,
|
||||||
dropIndex: PropTypes.number,
|
dragQualityIndex: PropTypes.string,
|
||||||
|
dropQualityIndex: PropTypes.string,
|
||||||
|
dropPosition: PropTypes.string,
|
||||||
qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
qualityProfileItems: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
errors: PropTypes.arrayOf(PropTypes.object),
|
errors: PropTypes.arrayOf(PropTypes.object),
|
||||||
warnings: PropTypes.arrayOf(PropTypes.object)
|
warnings: PropTypes.arrayOf(PropTypes.object),
|
||||||
|
onToggleEditGroupsMode: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
QualityProfileItems.defaultProps = {
|
QualityProfileItems.defaultProps = {
|
||||||
|
|
|
@ -91,7 +91,6 @@ class QualityProfiles extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
QualityProfiles.propTypes = {
|
QualityProfiles.propTypes = {
|
||||||
advancedSettings: PropTypes.bool.isRequired,
|
|
||||||
isFetching: PropTypes.bool.isRequired,
|
isFetching: PropTypes.bool.isRequired,
|
||||||
error: PropTypes.object,
|
error: PropTypes.object,
|
||||||
isDeleting: PropTypes.bool.isRequired,
|
isDeleting: PropTypes.bool.isRequired,
|
||||||
|
|
|
@ -7,11 +7,9 @@ import QualityProfiles from './QualityProfiles';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.settings.advancedSettings,
|
|
||||||
(state) => state.settings.qualityProfiles,
|
(state) => state.settings.qualityProfiles,
|
||||||
(advancedSettings, qualityProfiles) => {
|
(qualityProfiles) => {
|
||||||
return {
|
return {
|
||||||
advancedSettings,
|
|
||||||
...qualityProfiles
|
...qualityProfiles
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import { batchActions } from 'redux-batched-actions';
|
import { batchActions } from 'redux-batched-actions';
|
||||||
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import { set, update, updateItem } from '../baseActions';
|
import { set, update, updateItem } from '../baseActions';
|
||||||
|
|
||||||
function createFetchHandler(section, url) {
|
function createFetchHandler(section, url) {
|
||||||
|
@ -12,13 +12,13 @@ function createFetchHandler(section, url) {
|
||||||
...otherPayload
|
...otherPayload
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
const promise = $.ajax({
|
const { request, abortRequest } = createAjaxRequest({
|
||||||
url: id == null ? url : `${url}/${id}`,
|
url: id == null ? url : `${url}/${id}`,
|
||||||
data: otherPayload,
|
data: otherPayload,
|
||||||
traditional: true
|
traditional: true
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.done((data) => {
|
request.done((data) => {
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
id == null ? update({ section, data }) : updateItem({ section, ...data }),
|
id == null ? update({ section, data }) : updateItem({ section, ...data }),
|
||||||
|
|
||||||
|
@ -31,14 +31,16 @@ function createFetchHandler(section, url) {
|
||||||
]));
|
]));
|
||||||
});
|
});
|
||||||
|
|
||||||
promise.fail((xhr) => {
|
request.fail((xhr) => {
|
||||||
dispatch(set({
|
dispatch(set({
|
||||||
section,
|
section,
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: xhr
|
error: xhr.aborted ? null : xhr
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return abortRequest;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,8 +37,8 @@ function createSaveProviderHandler(section, url, getFromState) {
|
||||||
ajaxOptions.method = 'PUT';
|
ajaxOptions.method = 'PUT';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { request, abortRequest } = createAjaxRequest()(ajaxOptions);
|
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
|
||||||
|
|
||||||
abortCurrentRequests[section] = abortRequest;
|
abortCurrentRequests[section] = abortRequest;
|
||||||
|
|
||||||
request.done((data) => {
|
request.done((data) => {
|
||||||
|
|
|
@ -30,8 +30,8 @@ function createTestProviderHandler(section, url, getFromState) {
|
||||||
data: JSON.stringify(testData)
|
data: JSON.stringify(testData)
|
||||||
};
|
};
|
||||||
|
|
||||||
const { request, abortRequest } = createAjaxRequest()(ajaxOptions);
|
const { request, abortRequest } = createAjaxRequest(ajaxOptions);
|
||||||
|
|
||||||
abortCurrentRequests[section] = abortRequest;
|
abortCurrentRequests[section] = abortRequest;
|
||||||
|
|
||||||
request.done((data) => {
|
request.done((data) => {
|
||||||
|
|
|
@ -113,6 +113,7 @@ export const ALBUM_HISTORY_MARK_AS_FAILED = 'ALBUM_HISTORY_MARK_AS_FAILED';
|
||||||
// Releases
|
// Releases
|
||||||
|
|
||||||
export const FETCH_RELEASES = 'FETCH_RELEASES';
|
export const FETCH_RELEASES = 'FETCH_RELEASES';
|
||||||
|
export const CANCEL_FETCH_RELEASES = 'CANCEL_FETCH_RELEASES';
|
||||||
export const SET_RELEASES_SORT = 'SET_RELEASES_SORT';
|
export const SET_RELEASES_SORT = 'SET_RELEASES_SORT';
|
||||||
export const CLEAR_RELEASES = 'CLEAR_RELEASES';
|
export const CLEAR_RELEASES = 'CLEAR_RELEASES';
|
||||||
export const GRAB_RELEASE = 'GRAB_RELEASE';
|
export const GRAB_RELEASE = 'GRAB_RELEASE';
|
||||||
|
|
|
@ -18,7 +18,7 @@ const addArtistActionHandlers = {
|
||||||
abortCurrentRequest();
|
abortCurrentRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { request, abortRequest } = createAjaxRequest()({
|
const { request, abortRequest } = createAjaxRequest({
|
||||||
url: '/artist/lookup',
|
url: '/artist/lookup',
|
||||||
data: {
|
data: {
|
||||||
term: payload.term
|
term: payload.term
|
||||||
|
|
|
@ -3,10 +3,27 @@ import createFetchHandler from './Creators/createFetchHandler';
|
||||||
import * as types from './actionTypes';
|
import * as types from './actionTypes';
|
||||||
import { updateRelease } from './releaseActions';
|
import { updateRelease } from './releaseActions';
|
||||||
|
|
||||||
|
let abortCurrentRequest = null;
|
||||||
const section = 'releases';
|
const section = 'releases';
|
||||||
|
|
||||||
|
const fetchReleases = createFetchHandler(section, '/release');
|
||||||
|
|
||||||
const releaseActionHandlers = {
|
const releaseActionHandlers = {
|
||||||
[types.FETCH_RELEASES]: createFetchHandler(section, '/release'),
|
[types.FETCH_RELEASES]: function(payload) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
const abortRequest = fetchReleases(payload)(dispatch, getState);
|
||||||
|
|
||||||
|
abortCurrentRequest = abortRequest;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.CANCEL_FETCH_RELEASES]: function(payload) {
|
||||||
|
return function(dispatch, getState) {
|
||||||
|
if (abortCurrentRequest) {
|
||||||
|
abortCurrentRequest = abortCurrentRequest();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
[types.GRAB_RELEASE]: function(payload) {
|
[types.GRAB_RELEASE]: function(payload) {
|
||||||
return function(dispatch, getState) {
|
return function(dispatch, getState) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as types from './actionTypes';
|
||||||
import releaseActionHandlers from './releaseActionHandlers';
|
import releaseActionHandlers from './releaseActionHandlers';
|
||||||
|
|
||||||
export const fetchReleases = releaseActionHandlers[types.FETCH_RELEASES];
|
export const fetchReleases = releaseActionHandlers[types.FETCH_RELEASES];
|
||||||
|
export const cancelFetchReleases = releaseActionHandlers[types.CANCEL_FETCH_RELEASES];
|
||||||
export const setReleasesSort = createAction(types.SET_RELEASES_SORT);
|
export const setReleasesSort = createAction(types.SET_RELEASES_SORT);
|
||||||
export const clearReleases = createAction(types.CLEAR_RELEASES);
|
export const clearReleases = createAction(types.CLEAR_RELEASES);
|
||||||
export const grabRelease = releaseActionHandlers[types.GRAB_RELEASE];
|
export const grabRelease = releaseActionHandlers[types.GRAB_RELEASE];
|
||||||
|
|
|
@ -37,6 +37,11 @@ export const defaultState = {
|
||||||
label: 'Release Date',
|
label: 'Release Date',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'mediumCount',
|
||||||
|
label: 'Media Count',
|
||||||
|
isVisible: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'trackCount',
|
name: 'trackCount',
|
||||||
label: 'Track Count',
|
label: 'Track Count',
|
||||||
|
|
|
@ -11,14 +11,19 @@ export const defaultState = {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isPopulated: false,
|
isPopulated: false,
|
||||||
error: null,
|
error: null,
|
||||||
sortKey: 'trackNumber',
|
sortKey: 'mediumNumber',
|
||||||
sortDirection: sortDirections.DESCENDING,
|
sortDirection: sortDirections.DESCENDING,
|
||||||
items: [],
|
items: [],
|
||||||
|
|
||||||
columns: [
|
columns: [
|
||||||
{
|
{
|
||||||
name: 'trackNumber',
|
name: 'medium',
|
||||||
label: '#',
|
label: 'Medium',
|
||||||
|
isVisible: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'absoluteTrackNumber',
|
||||||
|
label: 'Track',
|
||||||
isVisible: true
|
isVisible: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -19,16 +19,21 @@ module.exports = {
|
||||||
breakpointSmall: '768px',
|
breakpointSmall: '768px',
|
||||||
breakpointMedium: '992px',
|
breakpointMedium: '992px',
|
||||||
breakpointLarge: '1200px',
|
breakpointLarge: '1200px',
|
||||||
|
breakpointExtraLarge: '1450px',
|
||||||
|
|
||||||
// Form
|
// Form
|
||||||
|
formGroupExtraSmallWidth: '550px',
|
||||||
formGroupSmallWidth: '650px',
|
formGroupSmallWidth: '650px',
|
||||||
formGroupMediumWidth: '800px',
|
formGroupMediumWidth: '800px',
|
||||||
formGroupLargeWidth: '1200px',
|
formGroupLargeWidth: '1200px',
|
||||||
formLabelWidth: '250px',
|
formLabelSmallWidth: '150px',
|
||||||
|
formLabelLargeWidth: '250px',
|
||||||
formLabelRightMarginWidth: '20px',
|
formLabelRightMarginWidth: '20px',
|
||||||
|
|
||||||
// Drag
|
// Drag
|
||||||
dragHandleWidth: '40px',
|
dragHandleWidth: '40px',
|
||||||
|
qualityProfileItemHeight: '30px',
|
||||||
|
qualityProfileItemDragSourcePadding: '4px',
|
||||||
|
|
||||||
// Progress Bar
|
// Progress Bar
|
||||||
progressBarSmallHeight: '5px',
|
progressBarSmallHeight: '5px',
|
||||||
|
@ -38,6 +43,9 @@ module.exports = {
|
||||||
// Jump Bar
|
// Jump Bar
|
||||||
jumpBarItemHeight: '25px',
|
jumpBarItemHeight: '25px',
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
modalBodyPadding: '30px',
|
||||||
|
|
||||||
// Artist
|
// Artist
|
||||||
artistIndexColumnPadding: '20px',
|
artistIndexColumnPadding: '20px',
|
||||||
artistIndexColumnPaddingSmallScreen: '10px',
|
artistIndexColumnPaddingSmallScreen: '10px',
|
||||||
|
|
|
@ -4,6 +4,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 getQualities from 'Utilities/Quality/getQualities';
|
||||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||||
import { deleteTrackFiles, updateTrackFiles } from 'Store/Actions/trackFileActions';
|
import { deleteTrackFiles, updateTrackFiles } from 'Store/Actions/trackFileActions';
|
||||||
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
import { fetchTracks, clearTracks } from 'Store/Actions/trackActions';
|
||||||
|
@ -52,8 +53,8 @@ function createMapStateToProps() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const languages = _.map(languageProfilesSchema.languages, 'language');
|
const languages = _.map(languageProfilesSchema.languages, 'language');
|
||||||
const qualities = _.map(qualityProfileSchema.items, 'quality');
|
const qualities = getQualities(qualityProfileSchema.items);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
artistType: artist.artistType,
|
artistType: artist.artistType,
|
||||||
|
@ -90,18 +91,6 @@ function createMapDispatchToProps(dispatch, props) {
|
||||||
|
|
||||||
onDeletePress(trackFileIds) {
|
onDeletePress(trackFileIds) {
|
||||||
dispatch(deleteTrackFiles({ trackFileIds }));
|
dispatch(deleteTrackFiles({ trackFileIds }));
|
||||||
},
|
|
||||||
|
|
||||||
onQualityChange(trackFileIds, qualityId) {
|
|
||||||
const quality = {
|
|
||||||
quality: _.find(this.props.qualities, { id: qualityId }),
|
|
||||||
revision: {
|
|
||||||
version: 1,
|
|
||||||
real: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
dispatch(updateTrackFiles({ trackFileIds, quality }));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ function TrackFileEditorRow(props) {
|
||||||
|
|
||||||
TrackFileEditorRow.propTypes = {
|
TrackFileEditorRow.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
trackNumber: PropTypes.number.isRequired,
|
trackNumber: PropTypes.string.isRequired,
|
||||||
relativePath: PropTypes.string.isRequired,
|
relativePath: PropTypes.string.isRequired,
|
||||||
language: PropTypes.object.isRequired,
|
language: PropTypes.object.isRequired,
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
|
|
16
frontend/src/Utilities/Quality/getQualities.js
Normal file
16
frontend/src/Utilities/Quality/getQualities.js
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
export default function getQualities(qualities) {
|
||||||
|
if (!qualities) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return qualities.reduce((acc, item) => {
|
||||||
|
if (item.quality) {
|
||||||
|
acc.push(item.quality);
|
||||||
|
} else {
|
||||||
|
const groupQualities = item.items.map((i) => i.quality);
|
||||||
|
acc.push(...groupQualities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
}
|
|
@ -1,32 +1,30 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
|
|
||||||
export default function createAjaxRequest() {
|
export default function createAjaxRequest(ajaxOptions) {
|
||||||
return function(ajaxOptions) {
|
const requestXHR = new window.XMLHttpRequest();
|
||||||
const requestXHR = new window.XMLHttpRequest();
|
let aborted = false;
|
||||||
let aborted = false;
|
let complete = false;
|
||||||
let complete = false;
|
|
||||||
|
|
||||||
function abortRequest() {
|
function abortRequest() {
|
||||||
if (!complete) {
|
if (!complete) {
|
||||||
aborted = true;
|
aborted = true;
|
||||||
requestXHR.abort();
|
requestXHR.abort();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const request = $.ajax({
|
const request = $.ajax({
|
||||||
xhr: () => requestXHR,
|
xhr: () => requestXHR,
|
||||||
...ajaxOptions
|
...ajaxOptions
|
||||||
}).then(null, (xhr, textStatus, errorThrown) => {
|
}).then(null, (xhr, textStatus, errorThrown) => {
|
||||||
xhr.aborted = aborted;
|
xhr.aborted = aborted;
|
||||||
|
|
||||||
return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
|
return $.Deferred().reject(xhr, textStatus, errorThrown).promise();
|
||||||
}).always(() => {
|
}).always(() => {
|
||||||
complete = true;
|
complete = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
request,
|
request,
|
||||||
abortRequest
|
abortRequest
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,7 +93,6 @@
|
||||||
"react-tag-autocomplete": "5.4.1",
|
"react-tag-autocomplete": "5.4.1",
|
||||||
"react-tether": "0.5.7",
|
"react-tether": "0.5.7",
|
||||||
"react-text-truncate": "0.12.0",
|
"react-text-truncate": "0.12.0",
|
||||||
"react-truncate": "2.2.2",
|
|
||||||
"react-virtualized": "9.10.1",
|
"react-virtualized": "9.10.1",
|
||||||
"redux": "3.7.2",
|
"redux": "3.7.2",
|
||||||
"redux-actions": "2.2.1",
|
"redux-actions": "2.2.1",
|
||||||
|
|
|
@ -20,9 +20,22 @@ namespace Lidarr.Api.V1.Albums
|
||||||
public int ProfileId { get; set; }
|
public int ProfileId { get; set; }
|
||||||
public int Duration { get; set; }
|
public int Duration { get; set; }
|
||||||
public string AlbumType { get; set; }
|
public string AlbumType { get; set; }
|
||||||
|
public int MediumCount
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Media == null)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Media.Where(s => s.MediumNumber > 0).Count();
|
||||||
|
}
|
||||||
|
}
|
||||||
public Ratings Ratings { get; set; }
|
public Ratings Ratings { get; set; }
|
||||||
public DateTime? ReleaseDate { get; set; }
|
public DateTime? ReleaseDate { get; set; }
|
||||||
public List<string> Genres { get; set; }
|
public List<string> Genres { get; set; }
|
||||||
|
public List<MediumResource> Media { get; set; }
|
||||||
public ArtistResource Artist { get; set; }
|
public ArtistResource Artist { get; set; }
|
||||||
public List<MediaCover> Images { get; set; }
|
public List<MediaCover> Images { get; set; }
|
||||||
public AlbumStatisticsResource Statistics { get; set; }
|
public AlbumStatisticsResource Statistics { get; set; }
|
||||||
|
@ -32,7 +45,7 @@ namespace Lidarr.Api.V1.Albums
|
||||||
public bool Grabbed { get; set; }
|
public bool Grabbed { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class EpisodeResourceMapper
|
public static class AlbumResourceMapper
|
||||||
{
|
{
|
||||||
public static AlbumResource ToResource(this Album model)
|
public static AlbumResource ToResource(this Album model)
|
||||||
{
|
{
|
||||||
|
@ -53,7 +66,8 @@ namespace Lidarr.Api.V1.Albums
|
||||||
Images = model.Images,
|
Images = model.Images,
|
||||||
Ratings = model.Ratings,
|
Ratings = model.Ratings,
|
||||||
Duration = model.Duration,
|
Duration = model.Duration,
|
||||||
AlbumType = model.AlbumType
|
AlbumType = model.AlbumType,
|
||||||
|
Media = model.Media.ToResource(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
56
src/Lidarr.Api.V1/Albums/MediumResource.cs
Normal file
56
src/Lidarr.Api.V1/Albums/MediumResource.cs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Music;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.Albums
|
||||||
|
{
|
||||||
|
public class MediumResource
|
||||||
|
{
|
||||||
|
public int MediumNumber { get; set; }
|
||||||
|
public string MediumName { get; set; }
|
||||||
|
public string MediumFormat { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SeasonResourceMapper
|
||||||
|
{
|
||||||
|
public static MediumResource ToResource(this Medium model)
|
||||||
|
{
|
||||||
|
if (model == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new MediumResource
|
||||||
|
{
|
||||||
|
MediumNumber = model.Number,
|
||||||
|
MediumName = model.Name,
|
||||||
|
MediumFormat = model.Format
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Medium ToModel(this MediumResource resource)
|
||||||
|
{
|
||||||
|
if (resource == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Medium
|
||||||
|
{
|
||||||
|
Number = resource.MediumNumber,
|
||||||
|
Name = resource.MediumName,
|
||||||
|
Format = resource.MediumFormat
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<MediumResource> ToResource(this IEnumerable<Medium> models)
|
||||||
|
{
|
||||||
|
return models.Select(ToResource).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Medium> ToModel(this IEnumerable<MediumResource> resources)
|
||||||
|
{
|
||||||
|
return resources.Select(ToModel).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Nancy;
|
using Nancy;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
using NzbDrone.Core.DecisionEngine;
|
using NzbDrone.Core.DecisionEngine;
|
||||||
|
@ -9,6 +11,7 @@ using Lidarr.Api.V1.Artist;
|
||||||
using Lidarr.Api.V1.Tracks;
|
using Lidarr.Api.V1.Tracks;
|
||||||
using Lidarr.Http;
|
using Lidarr.Http;
|
||||||
using Lidarr.Http.Extensions;
|
using Lidarr.Http.Extensions;
|
||||||
|
using Lidarr.Http.REST;
|
||||||
|
|
||||||
namespace Lidarr.Api.V1.History
|
namespace Lidarr.Api.V1.History
|
||||||
{
|
{
|
||||||
|
@ -27,6 +30,7 @@ namespace Lidarr.Api.V1.History
|
||||||
_failedDownloadService = failedDownloadService;
|
_failedDownloadService = failedDownloadService;
|
||||||
GetResourcePaged = GetHistory;
|
GetResourcePaged = GetHistory;
|
||||||
|
|
||||||
|
Get["/since"] = x => GetHistorySince();
|
||||||
Post["/failed"] = x => MarkAsFailed();
|
Post["/failed"] = x => MarkAsFailed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +85,30 @@ namespace Lidarr.Api.V1.History
|
||||||
return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
|
return ApplyToPage(_historyService.Paged, pagingSpec, h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<HistoryResource> GetHistorySince()
|
||||||
|
{
|
||||||
|
var queryDate = Request.Query.Date;
|
||||||
|
var queryEventType = Request.Query.EventType;
|
||||||
|
|
||||||
|
if (!queryDate.HasValue)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("date is missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime date = DateTime.Parse(queryDate.Value);
|
||||||
|
HistoryEventType? eventType = null;
|
||||||
|
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
|
||||||
|
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum");
|
||||||
|
var includeTrack = Request.GetBooleanQueryParameter("includeTrack");
|
||||||
|
|
||||||
|
if (queryEventType.HasValue)
|
||||||
|
{
|
||||||
|
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private Response MarkAsFailed()
|
private Response MarkAsFailed()
|
||||||
{
|
{
|
||||||
var id = (int)Request.Form.Id;
|
var id = (int)Request.Form.Id;
|
||||||
|
|
|
@ -89,6 +89,7 @@
|
||||||
<Compile Include="Albums\AlbumResource.cs" />
|
<Compile Include="Albums\AlbumResource.cs" />
|
||||||
<Compile Include="Albums\AlbumsMonitoredResource.cs" />
|
<Compile Include="Albums\AlbumsMonitoredResource.cs" />
|
||||||
<Compile Include="Albums\AlbumStatisticsResource.cs" />
|
<Compile Include="Albums\AlbumStatisticsResource.cs" />
|
||||||
|
<Compile Include="Albums\MediumResource.cs" />
|
||||||
<Compile Include="Blacklist\BlacklistModule.cs" />
|
<Compile Include="Blacklist\BlacklistModule.cs" />
|
||||||
<Compile Include="Blacklist\BlacklistResource.cs" />
|
<Compile Include="Blacklist\BlacklistResource.cs" />
|
||||||
<Compile Include="Calendar\CalendarFeedModule.cs" />
|
<Compile Include="Calendar\CalendarFeedModule.cs" />
|
||||||
|
@ -97,6 +98,8 @@
|
||||||
<Compile Include="Commands\CommandResource.cs" />
|
<Compile Include="Commands\CommandResource.cs" />
|
||||||
<Compile Include="Config\MetadataProviderConfigModule.cs" />
|
<Compile Include="Config\MetadataProviderConfigModule.cs" />
|
||||||
<Compile Include="Config\MetadataProviderConfigResource.cs" />
|
<Compile Include="Config\MetadataProviderConfigResource.cs" />
|
||||||
|
<Compile Include="Profiles\Quality\QualityCutoffValidator.cs" />
|
||||||
|
<Compile Include="Profiles\Quality\QualityItemsValidator.cs" />
|
||||||
<Compile Include="TrackFiles\TrackFileListResource.cs" />
|
<Compile Include="TrackFiles\TrackFileListResource.cs" />
|
||||||
<Compile Include="TrackFiles\MediaInfoResource.cs" />
|
<Compile Include="TrackFiles\MediaInfoResource.cs" />
|
||||||
<Compile Include="Indexers\ReleaseModuleBase.cs" />
|
<Compile Include="Indexers\ReleaseModuleBase.cs" />
|
||||||
|
@ -172,7 +175,6 @@
|
||||||
<Compile Include="Profiles\Quality\QualityProfileModule.cs" />
|
<Compile Include="Profiles\Quality\QualityProfileModule.cs" />
|
||||||
<Compile Include="Profiles\Quality\QualityProfileResource.cs" />
|
<Compile Include="Profiles\Quality\QualityProfileResource.cs" />
|
||||||
<Compile Include="Profiles\Quality\QualityProfileSchemaModule.cs" />
|
<Compile Include="Profiles\Quality\QualityProfileSchemaModule.cs" />
|
||||||
<Compile Include="Profiles\Quality\QualityProfileValidation.cs" />
|
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="ProviderModuleBase.cs" />
|
<Compile Include="ProviderModuleBase.cs" />
|
||||||
<Compile Include="ProviderResource.cs" />
|
<Compile Include="ProviderResource.cs" />
|
||||||
|
|
45
src/Lidarr.Api.V1/Profiles/Quality/QualityCutoffValidator.cs
Normal file
45
src/Lidarr.Api.V1/Profiles/Quality/QualityCutoffValidator.cs
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
|
{
|
||||||
|
public static class QualityCutoffValidator
|
||||||
|
{
|
||||||
|
public static IRuleBuilderOptions<T, int> ValidCutoff<T>(this IRuleBuilder<T, int> ruleBuilder)
|
||||||
|
{
|
||||||
|
return ruleBuilder.SetValidator(new ValidCutoffValidator<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ValidCutoffValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public ValidCutoffValidator()
|
||||||
|
: base("Cutoff must be an allowed quality or group")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var cutoff = (int)context.PropertyValue;
|
||||||
|
dynamic instance = context.ParentContext.InstanceToValidate;
|
||||||
|
var items = instance.Items as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
var cutoffItem = items.SingleOrDefault(i => i.Id == cutoff || (i.Quality != null && i.Quality.Id == cutoff));
|
||||||
|
|
||||||
|
if (cutoffItem == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cutoffItem.Allowed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
197
src/Lidarr.Api.V1/Profiles/Quality/QualityItemsValidator.cs
Normal file
197
src/Lidarr.Api.V1/Profiles/Quality/QualityItemsValidator.cs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using FluentValidation;
|
||||||
|
using FluentValidation.Validators;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
|
||||||
|
namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
|
{
|
||||||
|
public static class QualityItemsValidator
|
||||||
|
{
|
||||||
|
public static IRuleBuilderOptions<T, IList<QualityProfileQualityItemResource>> ValidItems<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder)
|
||||||
|
{
|
||||||
|
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
||||||
|
ruleBuilder.SetValidator(new AllowedValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new QualityNameValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new EmptyItemGroupNameValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new ItemGroupIdValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new UniqueIdValidator<T>());
|
||||||
|
ruleBuilder.SetValidator(new UniqueQualityIdValidator<T>());
|
||||||
|
return ruleBuilder.SetValidator(new ItemGroupNameValidator<T>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AllowedValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public AllowedValidator()
|
||||||
|
: base("Must contain at least one allowed quality")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var list = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (list == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list.Any(c => c.Allowed))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class EmptyItemGroupNameValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public EmptyItemGroupNameValidator()
|
||||||
|
: base("Groups must not be empty")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Items.Empty()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class QualityNameValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public QualityNameValidator()
|
||||||
|
: base("Individual qualities should not be named")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Name.IsNotNullOrWhiteSpace() && i.Quality != null))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemGroupNameValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public ItemGroupNameValidator()
|
||||||
|
: base("Groups must have a name")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Quality == null && i.Name.IsNullOrWhiteSpace()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ItemGroupIdValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public ItemGroupIdValidator()
|
||||||
|
: base("Groups must have an ID")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Any(i => i.Quality == null && i.Id == 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UniqueIdValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public UniqueIdValidator()
|
||||||
|
: base("Groups must have a unique ID")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
|
||||||
|
if (items.Where(i => i.Id > 0).Select(i => i.Id).GroupBy(i => i).Any(g => g.Count() > 1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UniqueQualityIdValidator<T> : PropertyValidator
|
||||||
|
{
|
||||||
|
public UniqueQualityIdValidator()
|
||||||
|
: base("Qualities can only be used once")
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool IsValid(PropertyValidatorContext context)
|
||||||
|
{
|
||||||
|
var items = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
||||||
|
var qualityIds = new HashSet<int>();
|
||||||
|
|
||||||
|
foreach (var item in items)
|
||||||
|
{
|
||||||
|
if (item.Id > 0)
|
||||||
|
{
|
||||||
|
foreach (var quality in item.Items)
|
||||||
|
{
|
||||||
|
if (qualityIds.Contains(quality.Quality.Id))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityIds.Add(quality.Quality.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (qualityIds.Contains(item.Quality.Id))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
qualityIds.Add(item.Quality.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using Lidarr.Http;
|
using Lidarr.Http;
|
||||||
|
@ -13,8 +13,10 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
{
|
{
|
||||||
_profileService = profileService;
|
_profileService = profileService;
|
||||||
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
SharedValidator.RuleFor(c => c.Name).NotEmpty();
|
||||||
SharedValidator.RuleFor(c => c.Cutoff).NotNull();
|
// TODO: Need to validate the cutoff is allowed and the ID/quality ID exists
|
||||||
SharedValidator.RuleFor(c => c.Items).MustHaveAllowedQuality();
|
// TODO: Need to validate the Items to ensure groups have names and at no item has no name, no items and no quality
|
||||||
|
SharedValidator.RuleFor(c => c.Cutoff).ValidCutoff();
|
||||||
|
SharedValidator.RuleFor(c => c.Items).ValidItems();
|
||||||
|
|
||||||
GetResourceAll = GetAll;
|
GetResourceAll = GetAll;
|
||||||
GetResourceById = GetById;
|
GetResourceById = GetById;
|
||||||
|
@ -52,4 +54,4 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
return _profileService.All().ToResource();
|
return _profileService.All().ToResource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using Lidarr.Http.REST;
|
using Lidarr.Http.REST;
|
||||||
|
@ -8,14 +8,21 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
public class QualityProfileResource : RestResource
|
public class QualityProfileResource : RestResource
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public NzbDrone.Core.Qualities.Quality Cutoff { get; set; }
|
public int Cutoff { get; set; }
|
||||||
public List<QualityProfileQualityItemResource> Items { get; set; }
|
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class QualityProfileQualityItemResource : RestResource
|
public class QualityProfileQualityItemResource : RestResource
|
||||||
{
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
|
public NzbDrone.Core.Qualities.Quality Quality { get; set; }
|
||||||
|
public List<QualityProfileQualityItemResource> Items { get; set; }
|
||||||
public bool Allowed { get; set; }
|
public bool Allowed { get; set; }
|
||||||
|
|
||||||
|
public QualityProfileQualityItemResource()
|
||||||
|
{
|
||||||
|
Items = new List<QualityProfileQualityItemResource>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ProfileResourceMapper
|
public static class ProfileResourceMapper
|
||||||
|
@ -27,7 +34,6 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
return new QualityProfileResource
|
return new QualityProfileResource
|
||||||
{
|
{
|
||||||
Id = model.Id,
|
Id = model.Id,
|
||||||
|
|
||||||
Name = model.Name,
|
Name = model.Name,
|
||||||
Cutoff = model.Cutoff,
|
Cutoff = model.Cutoff,
|
||||||
Items = model.Items.ConvertAll(ToResource),
|
Items = model.Items.ConvertAll(ToResource),
|
||||||
|
@ -40,7 +46,10 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
|
|
||||||
return new QualityProfileQualityItemResource
|
return new QualityProfileQualityItemResource
|
||||||
{
|
{
|
||||||
|
Id = model.Id,
|
||||||
|
Name = model.Name,
|
||||||
Quality = model.Quality,
|
Quality = model.Quality,
|
||||||
|
Items = model.Items.ConvertAll(ToResource),
|
||||||
Allowed = model.Allowed
|
Allowed = model.Allowed
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -52,9 +61,8 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
return new Profile
|
return new Profile
|
||||||
{
|
{
|
||||||
Id = resource.Id,
|
Id = resource.Id,
|
||||||
|
|
||||||
Name = resource.Name,
|
Name = resource.Name,
|
||||||
Cutoff = (NzbDrone.Core.Qualities.Quality)resource.Cutoff.Id,
|
Cutoff = resource.Cutoff,
|
||||||
Items = resource.Items.ConvertAll(ToModel)
|
Items = resource.Items.ConvertAll(ToModel)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -65,7 +73,10 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
|
|
||||||
return new ProfileQualityItem
|
return new ProfileQualityItem
|
||||||
{
|
{
|
||||||
Quality = (NzbDrone.Core.Qualities.Quality)resource.Quality.Id,
|
Id = resource.Id,
|
||||||
|
Name = resource.Name,
|
||||||
|
Quality = resource.Quality != null ? (NzbDrone.Core.Qualities.Quality)resource.Quality.Id : null,
|
||||||
|
Items = resource.Items.ConvertAll(ToModel),
|
||||||
Allowed = resource.Allowed
|
Allowed = resource.Allowed
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -75,4 +86,4 @@ namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
return models.Select(ToResource).ToList();
|
return models.Select(ToResource).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,52 @@
|
||||||
using System.Linq;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NzbDrone.Core.Profiles.Qualities;
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
using NzbDrone.Core.Qualities;
|
|
||||||
using Lidarr.Http;
|
using Lidarr.Http;
|
||||||
|
|
||||||
namespace Lidarr.Api.V1.Profiles.Quality
|
namespace Lidarr.Api.V1.Profiles.Quality
|
||||||
{
|
{
|
||||||
public class QualityProfileSchemaModule : LidarrRestModule<QualityProfileResource>
|
public class QualityProfileSchemaModule : LidarrRestModule<QualityProfileResource>
|
||||||
{
|
{
|
||||||
private readonly IQualityDefinitionService _qualityDefinitionService;
|
public QualityProfileSchemaModule()
|
||||||
|
|
||||||
public QualityProfileSchemaModule(IQualityDefinitionService qualityDefinitionService)
|
|
||||||
: base("/qualityprofile/schema")
|
: base("/qualityprofile/schema")
|
||||||
{
|
{
|
||||||
_qualityDefinitionService = qualityDefinitionService;
|
|
||||||
|
|
||||||
GetResourceSingle = GetSchema;
|
GetResourceSingle = GetSchema;
|
||||||
}
|
}
|
||||||
|
|
||||||
private QualityProfileResource GetSchema()
|
private QualityProfileResource GetSchema()
|
||||||
{
|
{
|
||||||
var items = _qualityDefinitionService.All()
|
var groupedQualites = NzbDrone.Core.Qualities.Quality.DefaultQualityDefinitions.GroupBy(q => q.Weight);
|
||||||
.OrderBy(v => v.Weight)
|
var items = new List<ProfileQualityItem>();
|
||||||
.Select(v => new ProfileQualityItem { Quality = v.Quality, Allowed = false })
|
var groupId = 1000;
|
||||||
.ToList();
|
|
||||||
|
foreach (var group in groupedQualites)
|
||||||
|
{
|
||||||
|
if (group.Count() == 1)
|
||||||
|
{
|
||||||
|
items.Add(new ProfileQualityItem { Quality = group.First().Quality, Allowed = false });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(new ProfileQualityItem
|
||||||
|
{
|
||||||
|
Id = groupId,
|
||||||
|
Name = group.First().GroupName,
|
||||||
|
Items = group.Select(g => new ProfileQualityItem
|
||||||
|
{
|
||||||
|
Quality = g.Quality,
|
||||||
|
Allowed = false
|
||||||
|
}).ToList(),
|
||||||
|
Allowed = false
|
||||||
|
});
|
||||||
|
|
||||||
|
groupId++;
|
||||||
|
}
|
||||||
|
|
||||||
var qualityProfile = new Profile();
|
var qualityProfile = new Profile();
|
||||||
qualityProfile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown;
|
qualityProfile.Cutoff = NzbDrone.Core.Qualities.Quality.Unknown.Id;
|
||||||
qualityProfile.Items = items;
|
qualityProfile.Items = items;
|
||||||
|
|
||||||
return qualityProfile.ToResource();
|
return qualityProfile.ToResource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using FluentValidation;
|
|
||||||
using FluentValidation.Validators;
|
|
||||||
|
|
||||||
namespace Lidarr.Api.V1.Profiles.Quality
|
|
||||||
{
|
|
||||||
public static class QualityProfileValidation
|
|
||||||
{
|
|
||||||
public static IRuleBuilderOptions<T, IList<QualityProfileQualityItemResource>> MustHaveAllowedQuality<T>(this IRuleBuilder<T, IList<QualityProfileQualityItemResource>> ruleBuilder)
|
|
||||||
{
|
|
||||||
ruleBuilder.SetValidator(new NotEmptyValidator(null));
|
|
||||||
|
|
||||||
return ruleBuilder.SetValidator(new AllowedValidator<T>());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class AllowedValidator<T> : PropertyValidator
|
|
||||||
{
|
|
||||||
public AllowedValidator()
|
|
||||||
: base("Must contain at least one allowed quality")
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool IsValid(PropertyValidatorContext context)
|
|
||||||
{
|
|
||||||
var list = context.PropertyValue as IList<QualityProfileQualityItemResource>;
|
|
||||||
|
|
||||||
if (list == null)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!list.Any(c => c.Allowed))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,14 +15,15 @@ namespace Lidarr.Api.V1.Tracks
|
||||||
public int TrackFileId { get; set; }
|
public int TrackFileId { get; set; }
|
||||||
public int AlbumId { get; set; }
|
public int AlbumId { get; set; }
|
||||||
public bool Explicit { get; set; }
|
public bool Explicit { get; set; }
|
||||||
public int TrackNumber { get; set; }
|
public int AbsoluteTrackNumber { get; set; }
|
||||||
|
public string TrackNumber { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public int Duration { get; set; }
|
public int Duration { get; set; }
|
||||||
public TrackFileResource TrackFile { get; set; }
|
public TrackFileResource TrackFile { get; set; }
|
||||||
|
public int MediumNumber { get; set; }
|
||||||
public bool HasFile { get; set; }
|
public bool HasFile { get; set; }
|
||||||
public bool Monitored { get; set; }
|
public bool Monitored { get; set; }
|
||||||
//public string SeriesTitle { get; set; }
|
|
||||||
public ArtistResource Artist { get; set; }
|
public ArtistResource Artist { get; set; }
|
||||||
public Ratings Ratings { get; set; }
|
public Ratings Ratings { get; set; }
|
||||||
|
|
||||||
|
@ -45,16 +46,14 @@ namespace Lidarr.Api.V1.Tracks
|
||||||
TrackFileId = model.TrackFileId,
|
TrackFileId = model.TrackFileId,
|
||||||
AlbumId = model.AlbumId,
|
AlbumId = model.AlbumId,
|
||||||
Explicit = model.Explicit,
|
Explicit = model.Explicit,
|
||||||
|
AbsoluteTrackNumber = model.AbsoluteTrackNumber,
|
||||||
TrackNumber = model.TrackNumber,
|
TrackNumber = model.TrackNumber,
|
||||||
Title = model.Title,
|
Title = model.Title,
|
||||||
Duration = model.Duration,
|
Duration = model.Duration,
|
||||||
//EpisodeFile
|
MediumNumber = model.MediumNumber,
|
||||||
|
|
||||||
HasFile = model.HasFile,
|
HasFile = model.HasFile,
|
||||||
Monitored = model.Monitored,
|
Monitored = model.Monitored,
|
||||||
Ratings = model.Ratings,
|
Ratings = model.Ratings,
|
||||||
//SeriesTitle = model.SeriesTitle,
|
|
||||||
//Series = model.Series.MapToResource(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ namespace NzbDrone.Core.Test.Datastore
|
||||||
var profile = new Profile
|
var profile = new Profile
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
new Profile
|
new Profile
|
||||||
|
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_256,
|
Cutoff = Quality.MP3_256.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||||
},
|
},
|
||||||
new LanguageProfile
|
new LanguageProfile
|
||||||
|
@ -37,7 +37,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
Subject.CutoffNotMet(
|
Subject.CutoffNotMet(
|
||||||
new Profile
|
new Profile
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_256,
|
Cutoff = Quality.MP3_256.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||||
},
|
},
|
||||||
new LanguageProfile
|
new LanguageProfile
|
||||||
|
@ -55,7 +55,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
new Profile
|
new Profile
|
||||||
|
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_256,
|
Cutoff = Quality.MP3_256.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||||
},
|
},
|
||||||
new LanguageProfile
|
new LanguageProfile
|
||||||
|
@ -73,7 +73,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
new Profile
|
new Profile
|
||||||
|
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||||
},
|
},
|
||||||
new LanguageProfile
|
new LanguageProfile
|
||||||
|
@ -93,7 +93,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
new Profile
|
new Profile
|
||||||
|
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities()
|
Items = Qualities.QualityFixture.GetDefaultQualities()
|
||||||
},
|
},
|
||||||
new LanguageProfile
|
new LanguageProfile
|
||||||
|
@ -111,7 +111,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Profile _profile = new Profile
|
Profile _profile = new Profile
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Profile _profile = new Profile
|
Profile _profile = new Profile
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Profile _profile = new Profile
|
Profile _profile = new Profile
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Profile _profile = new Profile
|
Profile _profile = new Profile
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
|
||||||
Profile _profile = new Profile
|
Profile _profile = new Profile
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -47,7 +47,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
};
|
};
|
||||||
|
|
||||||
_fakeArtist = Builder<Artist>.CreateNew()
|
_fakeArtist = Builder<Artist>.CreateNew()
|
||||||
.With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() })
|
.With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512.Id, Items = Qualities.QualityFixture.GetDefaultQualities() })
|
||||||
.With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = LanguageFixture.GetDefaultLanguages() })
|
.With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = LanguageFixture.GetDefaultLanguages() })
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
@ -162,7 +162,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_not_be_upgradable_if_album_is_of_same_quality_as_existing()
|
public void should_not_be_upgradable_if_album_is_of_same_quality_as_existing()
|
||||||
{
|
{
|
||||||
_fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
_fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512.Id, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||||
_parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1));
|
_parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1));
|
||||||
_upgradableQuality = new Tuple<QualityModel, Language>(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.English);
|
_upgradableQuality = new Tuple<QualityModel, Language>(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.English);
|
||||||
|
|
||||||
|
@ -174,7 +174,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_not_be_upgradable_if_cutoff_already_met()
|
public void should_not_be_upgradable_if_cutoff_already_met()
|
||||||
{
|
{
|
||||||
_fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
_fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512.Id, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||||
_parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1));
|
_parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1));
|
||||||
_upgradableQuality = new Tuple<QualityModel, Language>(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish);
|
_upgradableQuality = new Tuple<QualityModel, Language>(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish);
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
public void should_return_false_if_cutoff_already_met_and_cdh_is_disabled()
|
public void should_return_false_if_cutoff_already_met_and_cdh_is_disabled()
|
||||||
{
|
{
|
||||||
GivenCdhDisabled();
|
GivenCdhDisabled();
|
||||||
_fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
_fakeArtist.Profile = new Profile { Cutoff = Quality.MP3_512.Id, Items = Qualities.QualityFixture.GetDefaultQualities() };
|
||||||
_parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1));
|
_parseResultSingle.ParsedAlbumInfo.Quality = new QualityModel(Quality.MP3_512, new Revision(version: 1));
|
||||||
_upgradableQuality = new Tuple<QualityModel, Language>(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish);
|
_upgradableQuality = new Tuple<QualityModel, Language>(new QualityModel(Quality.MP3_512, new Revision(version: 1)), Language.Spanish);
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
var fakeArtist = Builder<Artist>.CreateNew()
|
var fakeArtist = Builder<Artist>.CreateNew()
|
||||||
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.MP3_512 })
|
.With(c => c.Profile = (LazyLoaded<Profile>)new Profile { Cutoff = Quality.MP3_512.Id })
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
remoteAlbum = new RemoteAlbum
|
remoteAlbum = new RemoteAlbum
|
||||||
|
|
|
@ -79,7 +79,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
var profile = new Profile
|
var profile = new Profile
|
||||||
{
|
{
|
||||||
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
Items = Qualities.QualityFixture.GetDefaultQualities(),
|
||||||
Cutoff = cutoff,
|
Cutoff = cutoff.Id,
|
||||||
};
|
};
|
||||||
|
|
||||||
var langProfile = new LanguageProfile
|
var langProfile = new LanguageProfile
|
||||||
|
|
|
@ -103,7 +103,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_when_quality_in_queue_is_lower()
|
public void should_return_true_when_quality_in_queue_is_lower()
|
||||||
{
|
{
|
||||||
_artist.Profile.Value.Cutoff = Quality.MP3_512;
|
_artist.Profile.Value.Cutoff = Quality.MP3_512.Id;
|
||||||
_artist.LanguageProfile.Value.Cutoff = Language.Spanish;
|
_artist.LanguageProfile.Value.Cutoff = Language.Spanish;
|
||||||
|
|
||||||
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
||||||
|
@ -123,7 +123,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_true_when_quality_in_queue_is_lower_but_language_is_higher()
|
public void should_return_true_when_quality_in_queue_is_lower_but_language_is_higher()
|
||||||
{
|
{
|
||||||
_artist.Profile.Value.Cutoff = Quality.FLAC;
|
_artist.Profile.Value.Cutoff = Quality.FLAC.Id;
|
||||||
_artist.LanguageProfile.Value.Cutoff = Language.Spanish;
|
_artist.LanguageProfile.Value.Cutoff = Language.Spanish;
|
||||||
|
|
||||||
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
||||||
|
@ -193,7 +193,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_false_when_quality_in_queue_is_better()
|
public void should_return_false_when_quality_in_queue_is_better()
|
||||||
{
|
{
|
||||||
_artist.Profile.Value.Cutoff = Quality.MP3_512;
|
_artist.Profile.Value.Cutoff = Quality.MP3_512.Id;
|
||||||
|
|
||||||
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
||||||
.With(r => r.Artist = _artist)
|
.With(r => r.Artist = _artist)
|
||||||
|
@ -289,7 +289,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
[Test]
|
[Test]
|
||||||
public void should_return_false_if_quality_and_language_in_queue_meets_cutoff()
|
public void should_return_false_if_quality_and_language_in_queue_meets_cutoff()
|
||||||
{
|
{
|
||||||
_artist.Profile.Value.Cutoff = _remoteAlbum.ParsedAlbumInfo.Quality.Quality;
|
_artist.Profile.Value.Cutoff = _remoteAlbum.ParsedAlbumInfo.Quality.Quality.Id;
|
||||||
|
|
||||||
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
var remoteAlbum = Builder<RemoteAlbum>.CreateNew()
|
||||||
.With(r => r.Artist = _artist)
|
.With(r => r.Artist = _artist)
|
||||||
|
|
|
@ -59,7 +59,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||||
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 });
|
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 });
|
||||||
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 });
|
_profile.Items.Add(new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_320 });
|
||||||
|
|
||||||
_profile.Cutoff = Quality.MP3_320;
|
_profile.Cutoff = Quality.MP3_320.Id;
|
||||||
|
|
||||||
_langProfile.Cutoff = Language.Spanish;
|
_langProfile.Cutoff = Language.Spanish;
|
||||||
_langProfile.Languages = Languages.LanguageFixture.GetDefaultLanguages();
|
_langProfile.Languages = Languages.LanguageFixture.GetDefaultLanguages();
|
||||||
|
|
|
@ -60,7 +60,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||||
var secondTrack = new Track { TrackFile = _secondFile, TrackFileId = 2, AlbumId = 2 };
|
var secondTrack = new Track { TrackFile = _secondFile, TrackFileId = 2, AlbumId = 2 };
|
||||||
|
|
||||||
var fakeArtist = Builder<Artist>.CreateNew()
|
var fakeArtist = Builder<Artist>.CreateNew()
|
||||||
.With(c => c.Profile = new Profile { Cutoff = Quality.FLAC })
|
.With(c => c.Profile = new Profile { Cutoff = Quality.FLAC.Id })
|
||||||
.With(c => c.Path = @"C:\Music\My.Artist".AsOsAgnostic())
|
.With(c => c.Path = @"C:\Music\My.Artist".AsOsAgnostic())
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync
|
||||||
|
|
||||||
|
|
||||||
var fakeArtist = Builder<Artist>.CreateNew()
|
var fakeArtist = Builder<Artist>.CreateNew()
|
||||||
.With(c => c.Profile = new Profile { Cutoff = Quality.FLAC })
|
.With(c => c.Profile = new Profile { Cutoff = Quality.FLAC.Id })
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
Mocker.GetMock<IMediaFileService>()
|
Mocker.GetMock<IMediaFileService>()
|
||||||
|
|
|
@ -40,7 +40,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
var languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish);
|
var languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English, Language.Spanish);
|
||||||
|
|
||||||
var fakeArtist = Builder<Artist>.CreateNew()
|
var fakeArtist = Builder<Artist>.CreateNew()
|
||||||
.With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512, Items = Qualities.QualityFixture.GetDefaultQualities()})
|
.With(c => c.Profile = new Profile { Cutoff = Quality.MP3_512.Id, Items = Qualities.QualityFixture.GetDefaultQualities()})
|
||||||
.With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = languages })
|
.With(l => l.LanguageProfile = new LanguageProfile { Cutoff = Language.Spanish, Languages = languages })
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||||
_profile = new Profile
|
_profile = new Profile
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Cutoff = Quality.MP3_256,
|
Cutoff = Quality.MP3_256.Id,
|
||||||
Items = new List<ProfileQualityItem>
|
Items = new List<ProfileQualityItem>
|
||||||
{
|
{
|
||||||
new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 },
|
new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 },
|
||||||
|
|
|
@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||||
_profile = new Profile
|
_profile = new Profile
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Cutoff = Quality.MP3_256,
|
Cutoff = Quality.MP3_256.Id,
|
||||||
Items = new List<ProfileQualityItem>
|
Items = new List<ProfileQualityItem>
|
||||||
{
|
{
|
||||||
new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 },
|
new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_256 },
|
||||||
|
|
|
@ -41,7 +41,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
|
||||||
_profile = new Profile
|
_profile = new Profile
|
||||||
{
|
{
|
||||||
Name = "Test",
|
Name = "Test",
|
||||||
Cutoff = Quality.MP3_192,
|
Cutoff = Quality.MP3_192.Id,
|
||||||
Items = new List<ProfileQualityItem>
|
Items = new List<ProfileQualityItem>
|
||||||
{
|
{
|
||||||
new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_192 },
|
new ProfileQualityItem { Allowed = true, Quality = Quality.MP3_192 },
|
||||||
|
|
|
@ -30,14 +30,14 @@ namespace NzbDrone.Core.Test.HistoryTests
|
||||||
{
|
{
|
||||||
_profile = new Profile
|
_profile = new Profile
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = QualityFixture.GetDefaultQualities(),
|
Items = QualityFixture.GetDefaultQualities(),
|
||||||
};
|
};
|
||||||
|
|
||||||
_profileCustom = new Profile
|
_profileCustom = new Profile
|
||||||
|
|
||||||
{
|
{
|
||||||
Cutoff = Quality.MP3_320,
|
Cutoff = Quality.MP3_320.Id,
|
||||||
Items = QualityFixture.GetDefaultQualities(Quality.MP3_256),
|
Items = QualityFixture.GetDefaultQualities(Quality.MP3_256),
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using FluentAssertions;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Languages;
|
||||||
|
using NzbDrone.Core.Profiles.Qualities;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.Music;
|
||||||
|
using NzbDrone.Core.Profiles.Languages;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.MusicTests.ArtistRepositoryTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
|
||||||
|
public class ArtistRepositoryFixture : DbTest<ArtistRepository, Artist>
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void should_lazyload_quality_profile()
|
||||||
|
{
|
||||||
|
var profile = new Profile
|
||||||
|
{
|
||||||
|
Items = Qualities.QualityFixture.GetDefaultQualities(Quality.FLAC, Quality.MP3_192, Quality.MP3_320),
|
||||||
|
|
||||||
|
Cutoff = Quality.FLAC.Id,
|
||||||
|
Name = "TestProfile"
|
||||||
|
};
|
||||||
|
|
||||||
|
var langProfile = new LanguageProfile
|
||||||
|
{
|
||||||
|
Name = "TestProfile",
|
||||||
|
Languages = Languages.LanguageFixture.GetDefaultLanguages(Language.English),
|
||||||
|
Cutoff = Language.English
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
Mocker.Resolve<ProfileRepository>().Insert(profile);
|
||||||
|
Mocker.Resolve<LanguageProfileRepository>().Insert(langProfile);
|
||||||
|
|
||||||
|
var series = Builder<Artist>.CreateNew().BuildNew();
|
||||||
|
series.ProfileId = profile.Id;
|
||||||
|
series.LanguageProfileId = langProfile.Id;
|
||||||
|
|
||||||
|
Subject.Insert(series);
|
||||||
|
|
||||||
|
|
||||||
|
StoredModel.Profile.Should().NotBeNull();
|
||||||
|
StoredModel.LanguageProfile.Should().NotBeNull();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -270,11 +270,14 @@
|
||||||
<Compile Include="MetadataSource\SkyHook\SkyHookProxyFixture.cs" />
|
<Compile Include="MetadataSource\SkyHook\SkyHookProxyFixture.cs" />
|
||||||
<Compile Include="MusicTests\AddArtistFixture.cs" />
|
<Compile Include="MusicTests\AddArtistFixture.cs" />
|
||||||
<Compile Include="MusicTests\ArtistNameSlugValidatorFixture.cs" />
|
<Compile Include="MusicTests\ArtistNameSlugValidatorFixture.cs" />
|
||||||
|
<Compile Include="MusicTests\ArtistRepositoryTests\ArtistRepositoryFixture.cs" />
|
||||||
<Compile Include="NotificationTests\NotificationBaseFixture.cs" />
|
<Compile Include="NotificationTests\NotificationBaseFixture.cs" />
|
||||||
<Compile Include="NotificationTests\SynologyIndexerFixture.cs" />
|
<Compile Include="NotificationTests\SynologyIndexerFixture.cs" />
|
||||||
<Compile Include="OrganizerTests\FileNameBuilderTests\CleanTitleFixture.cs" />
|
<Compile Include="OrganizerTests\FileNameBuilderTests\CleanTitleFixture.cs" />
|
||||||
<Compile Include="OrganizerTests\FileNameBuilderTests\TitleTheFixture.cs" />
|
<Compile Include="OrganizerTests\FileNameBuilderTests\TitleTheFixture.cs" />
|
||||||
<Compile Include="ParserTests\MusicParserFixture.cs" />
|
<Compile Include="ParserTests\MusicParserFixture.cs" />
|
||||||
|
<Compile Include="Profiles\Delay\DelayProfileServiceFixture.cs" />
|
||||||
|
<Compile Include="Profiles\Qualities\QualityIndexCompareToFixture.cs" />
|
||||||
<Compile Include="Qualities\RevisionComparableFixture.cs" />
|
<Compile Include="Qualities\RevisionComparableFixture.cs" />
|
||||||
<Compile Include="QueueTests\QueueServiceFixture.cs" />
|
<Compile Include="QueueTests\QueueServiceFixture.cs" />
|
||||||
<Compile Include="RemotePathMappingsTests\RemotePathMappingServiceFixture.cs" />
|
<Compile Include="RemotePathMappingsTests\RemotePathMappingServiceFixture.cs" />
|
||||||
|
|
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