mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-12 16:13:58 -07:00
UI Updates (Cancel Import, Move Artist, Manual Import from Artist)
Ability to cancel an import lookup/search at any point. Ability to move artist path from Artist Edit or bulk move from Mass Editor. Trigger manual import for Artist path from Artist Detail page. Pulled from Sonarr
This commit is contained in:
parent
5fae202760
commit
d8c89f5bbd
79 changed files with 1075 additions and 376 deletions
|
@ -58,7 +58,9 @@ class HistoryRow extends Component {
|
|||
album,
|
||||
track,
|
||||
language,
|
||||
languageCutoffNotMet,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
eventType,
|
||||
sourceTitle,
|
||||
date,
|
||||
|
@ -135,6 +137,7 @@ class HistoryRow extends Component {
|
|||
<TableRowCell key={name}>
|
||||
<EpisodeLanguage
|
||||
language={language}
|
||||
isCutoffMet={languageCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
|
@ -145,6 +148,7 @@ class HistoryRow extends Component {
|
|||
<TableRowCell key={name}>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
|
@ -233,7 +237,9 @@ HistoryRow.propTypes = {
|
|||
album: PropTypes.object,
|
||||
track: PropTypes.object,
|
||||
language: PropTypes.object.isRequired,
|
||||
languageCutoffNotMet: PropTypes.bool.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
|
||||
.queue-status-cell .popover {
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.queue {
|
||||
.protocol-cell {
|
||||
text-align: center;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.episode-number-cell {
|
||||
min-width: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
.remove-from-queue-modal {
|
||||
.form-horizontal {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.history-detail-modal {
|
||||
.info {
|
||||
word-wrap: break-word;
|
||||
}
|
||||
}
|
|
@ -19,6 +19,12 @@
|
|||
height: 35px;
|
||||
}
|
||||
|
||||
.loadingButton {
|
||||
composes: importButton;
|
||||
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
composes: loading from 'Components/Loading/LoadingIndicator.css';
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
|
@ -117,7 +118,8 @@ class ImportArtistFooter extends Component {
|
|||
isMetadataProfileIdMixed,
|
||||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
onImportPress
|
||||
onImportPress,
|
||||
onCancelLookupPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -227,10 +229,21 @@ class ImportArtistFooter extends Component {
|
|||
|
||||
{
|
||||
isLookingUpArtist &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={24}
|
||||
/>
|
||||
<Button
|
||||
className={styles.loadingButton}
|
||||
kind={kinds.WARNING}
|
||||
onPress={onCancelLookupPress}
|
||||
>
|
||||
Cancel Processing
|
||||
</Button>
|
||||
}
|
||||
|
||||
{
|
||||
isLookingUpArtist &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={24}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -261,7 +274,8 @@ ImportArtistFooter.propTypes = {
|
|||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onImportPress: PropTypes.func.isRequired
|
||||
onImportPress: PropTypes.func.isRequired,
|
||||
onCancelLookupPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportArtistFooter;
|
||||
|
|
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import ImportArtistFooter from './ImportArtistFooter';
|
||||
import { cancelLookupArtist } from 'Store/Actions/importArtistActions';
|
||||
|
||||
function isMixed(items, selectedIds, defaultValue, key) {
|
||||
return _.some(items, (artist) => {
|
||||
|
@ -23,11 +24,11 @@ function createMapStateToProps() {
|
|||
albumFolder: defaultAlbumFolder
|
||||
} = addArtist.defaults;
|
||||
|
||||
const items = importArtist.items;
|
||||
|
||||
const isLookingUpArtist = _.some(importArtist.items, (artist) => {
|
||||
return !artist.isPopulated && artist.error == null;
|
||||
});
|
||||
const {
|
||||
isLookingUpArtist,
|
||||
isImporting,
|
||||
items
|
||||
} = importArtist;
|
||||
|
||||
const isMonitorMixed = isMixed(items, selectedIds, defaultMonitor, 'monitor');
|
||||
const isQualityProfileIdMixed = isMixed(items, selectedIds, defaultQualityProfileId, 'qualityProfileId');
|
||||
|
@ -37,8 +38,8 @@ function createMapStateToProps() {
|
|||
|
||||
return {
|
||||
selectedCount: selectedIds.length,
|
||||
isImporting: importArtist.isImporting,
|
||||
isLookingUpArtist,
|
||||
isImporting,
|
||||
defaultMonitor,
|
||||
defaultQualityProfileId,
|
||||
defaultLanguageProfileId,
|
||||
|
@ -54,4 +55,8 @@ function createMapStateToProps() {
|
|||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(ImportArtistFooter);
|
||||
const mapDispatchToProps = {
|
||||
onCancelLookupPress: cancelLookupArtist
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistFooter);
|
||||
|
|
|
@ -10,12 +10,6 @@ class ImportArtistTable extends Component {
|
|||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._table = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
unmappedFolders,
|
||||
|
@ -101,22 +95,11 @@ class ImportArtistTable extends Component {
|
|||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Forces the table to re-render if the selected state
|
||||
// has changed otherwise it will be stale.
|
||||
|
||||
if (prevProps.selectedState !== selectedState && this._table) {
|
||||
this._table.forceUpdateGrid();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setTableRef = (ref) => {
|
||||
this._table = ref;
|
||||
}
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
rootFolderId,
|
||||
|
@ -156,6 +139,7 @@ class ImportArtistTable extends Component {
|
|||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
scrollTop,
|
||||
selectedState,
|
||||
onSelectAllChange,
|
||||
onScroll
|
||||
} = this.props;
|
||||
|
@ -166,7 +150,6 @@ class ImportArtistTable extends Component {
|
|||
|
||||
return (
|
||||
<VirtualTable
|
||||
ref={this.setTableRef}
|
||||
items={items}
|
||||
contentBody={contentBody}
|
||||
isSmallScreen={isSmallScreen}
|
||||
|
@ -183,6 +166,7 @@ class ImportArtistTable extends Component {
|
|||
onSelectAllChange={onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -66,6 +66,5 @@
|
|||
.searchInput {
|
||||
composes: text from 'Components/Form/TextInput.css';
|
||||
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import TetherComponent from 'react-tether';
|
|||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import SpinnerIcon from 'Components/SpinnerIcon';
|
||||
import FormInputButton from 'Components/Form/FormInputButton';
|
||||
import Link from 'Components/Link/Link';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
|
@ -99,6 +100,10 @@ class ImportArtistSelectArtist extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.onSearchInputChange(this.state.term);
|
||||
}
|
||||
|
||||
onArtistSelect = (foreignArtistId) => {
|
||||
this.setState({ isOpen: false });
|
||||
|
||||
|
@ -116,7 +121,8 @@ class ImportArtistSelectArtist extends Component {
|
|||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
queued
|
||||
queued,
|
||||
isLookingUpArtist
|
||||
} = this.props;
|
||||
|
||||
const errorMessage = error &&
|
||||
|
@ -137,7 +143,7 @@ class ImportArtistSelectArtist extends Component {
|
|||
onPress={this.onPress}
|
||||
>
|
||||
{
|
||||
queued && !isPopulated &&
|
||||
isLookingUpArtist && queued && !isPopulated &&
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
|
@ -206,10 +212,7 @@ class ImportArtistSelectArtist extends Component {
|
|||
<div className={styles.content}>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<SpinnerIcon
|
||||
name={icons.SEARCH}
|
||||
isSpinning={isFetching}
|
||||
/>
|
||||
<Icon name={icons.SEARCH} />
|
||||
</div>
|
||||
|
||||
<TextInput
|
||||
|
@ -218,6 +221,16 @@ class ImportArtistSelectArtist extends Component {
|
|||
value={this.state.term}
|
||||
onChange={this.onSearchInputChange}
|
||||
/>
|
||||
|
||||
<FormInputButton
|
||||
kind={kinds.DEFAULT}
|
||||
spinnerIcon={icons.REFRESH}
|
||||
canSpin={true}
|
||||
isSpinning={isFetching}
|
||||
onPress={this.onRefreshPress}
|
||||
>
|
||||
<Icon name={icons.REFRESH} />
|
||||
</FormInputButton>
|
||||
</div>
|
||||
|
||||
<div className={styles.results}>
|
||||
|
@ -253,6 +266,7 @@ ImportArtistSelectArtist.propTypes = {
|
|||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
queued: PropTypes.bool.isRequired,
|
||||
isLookingUpArtist: PropTypes.bool.isRequired,
|
||||
onSearchInputChange: PropTypes.func.isRequired,
|
||||
onArtistSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
|
|
@ -9,9 +9,13 @@ import ImportArtistSelectArtist from './ImportArtistSelectArtist';
|
|||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.importArtist.isLookingUpArtist,
|
||||
createImportArtistItemSelector(),
|
||||
(item) => {
|
||||
return item;
|
||||
(isLookingUpArtist, item) => {
|
||||
return {
|
||||
isLookingUpArtist,
|
||||
...item
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -29,7 +33,8 @@ class ImportArtistSelectArtistConnector extends Component {
|
|||
onSearchInputChange = (term) => {
|
||||
this.props.queueLookupArtist({
|
||||
name: this.props.id,
|
||||
term
|
||||
term,
|
||||
topOfQueue: true
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
|
||||
function EpisodeLanguage(props) {
|
||||
const {
|
||||
className,
|
||||
language
|
||||
language,
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
|
||||
if (!language) {
|
||||
|
@ -13,7 +15,10 @@ function EpisodeLanguage(props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<Label className={className}>
|
||||
<Label
|
||||
className={className}
|
||||
kind={isCutoffNotMet ? kinds.INVERSE : kinds.DEFAULT}
|
||||
>
|
||||
{language.name}
|
||||
</Label>
|
||||
);
|
||||
|
@ -21,7 +26,12 @@ function EpisodeLanguage(props) {
|
|||
|
||||
EpisodeLanguage.propTypes = {
|
||||
className: PropTypes.string,
|
||||
language: PropTypes.object
|
||||
language: PropTypes.object,
|
||||
isCutoffNotMet: PropTypes.bool
|
||||
};
|
||||
|
||||
EpisodeLanguage.defaultProps = {
|
||||
isCutoffNotMet: true
|
||||
};
|
||||
|
||||
export default EpisodeLanguage;
|
||||
|
|
|
@ -100,7 +100,8 @@ EpisodeNumber.propTypes = {
|
|||
};
|
||||
|
||||
EpisodeNumber.defaultProps = {
|
||||
unverifiedSceneNumbering: false
|
||||
unverifiedSceneNumbering: false,
|
||||
alternateTitles: []
|
||||
};
|
||||
|
||||
export default EpisodeNumber;
|
||||
|
|
|
@ -15,6 +15,11 @@ const columns = [
|
|||
label: 'Source Title',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: 'Language',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
|
|
|
@ -8,6 +8,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
|
@ -61,6 +62,8 @@ class AlbumHistoryRow extends Component {
|
|||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
language,
|
||||
languageCutoffNotMet,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
date,
|
||||
|
@ -83,6 +86,14 @@ class AlbumHistoryRow extends Component {
|
|||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeLanguage
|
||||
language={language}
|
||||
isCutoffNotMet={languageCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
|
@ -140,6 +151,8 @@ AlbumHistoryRow.propTypes = {
|
|||
id: PropTypes.number.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
languageCutoffNotMet: PropTypes.bool.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
@import "Content/icons";
|
||||
|
||||
.delete-artist-modal {
|
||||
|
||||
i {
|
||||
margin-right : 5px;
|
||||
//.fa-icon-color(white);
|
||||
|
||||
}
|
||||
|
||||
.path {
|
||||
white-space : nowrap;
|
||||
font-size : 16px;
|
||||
padding-bottom : 20px;
|
||||
}
|
||||
|
||||
.delete-files-info,
|
||||
.delete-label {
|
||||
color : @brand-danger-dark;
|
||||
}
|
||||
|
||||
.delete-files-info {
|
||||
display : none;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
display : inline-block;
|
||||
}
|
||||
|
||||
.c-checkbox:hover .check {
|
||||
border-color : @brand-danger-dark;
|
||||
}
|
||||
|
||||
input[type=checkbox]:checked + span {
|
||||
background-color : @brand-danger-dark;
|
||||
border-color : @brand-danger-dark;
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
|
|||
import ArtistTagsConnector from './ArtistTagsConnector';
|
||||
import ArtistDetailsLinks from './ArtistDetailsLinks';
|
||||
import styles from './ArtistDetails.css';
|
||||
import InteractiveImportModal from '../../InteractiveImport/InteractiveImportModal';
|
||||
|
||||
const albumTypes = [
|
||||
{
|
||||
|
@ -94,6 +95,7 @@ class ArtistDetails extends Component {
|
|||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false,
|
||||
isArtistHistoryModalOpen: false,
|
||||
isInteractiveImportModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {}
|
||||
|
@ -119,6 +121,14 @@ class ArtistDetails extends Component {
|
|||
this.setState({ isManageEpisodesOpen: false });
|
||||
}
|
||||
|
||||
onInteractiveImportPress = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: true });
|
||||
}
|
||||
|
||||
onInteractiveImportModalClose = () => {
|
||||
this.setState({ isInteractiveImportModalOpen: false });
|
||||
}
|
||||
|
||||
onEditArtistPress = () => {
|
||||
this.setState({ isEditArtistModalOpen: true });
|
||||
}
|
||||
|
@ -207,6 +217,7 @@ class ArtistDetails extends Component {
|
|||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen,
|
||||
isArtistHistoryModalOpen,
|
||||
isInteractiveImportModalOpen,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
expandedState
|
||||
|
@ -270,6 +281,12 @@ class ArtistDetails extends Component {
|
|||
onPress={this.onArtistHistoryPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manual Import"
|
||||
iconName={icons.INTERACTIVE}
|
||||
onPress={this.onInteractiveImportPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
|
@ -574,6 +591,13 @@ class ArtistDetails extends Component {
|
|||
artistId={id}
|
||||
onModalClose={this.onDeleteArtistModalClose}
|
||||
/>
|
||||
|
||||
<InteractiveImportModal
|
||||
isOpen={isInteractiveImportModalOpen}
|
||||
folder={path}
|
||||
showFilterExistingFiles={true}
|
||||
onModalClose={this.onInteractiveImportModalClose}
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
|
@ -11,11 +11,47 @@ import Form from 'Components/Form/Form';
|
|||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal';
|
||||
import styles from './EditArtistModalContent.css';
|
||||
|
||||
class EditArtistModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmMoveModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSavePress = () => {
|
||||
const {
|
||||
isPathChanging,
|
||||
onSavePress
|
||||
} = this.props;
|
||||
|
||||
if (isPathChanging && !this.state.isConfirmMoveModalOpen) {
|
||||
this.setState({ isConfirmMoveModalOpen: true });
|
||||
} else {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
onSavePress(false);
|
||||
}
|
||||
}
|
||||
|
||||
onMoveArtistPress = () => {
|
||||
this.setState({ isConfirmMoveModalOpen: false });
|
||||
|
||||
this.props.onSavePress(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
@ -25,8 +61,8 @@ class EditArtistModalContent extends Component {
|
|||
isSaving,
|
||||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
originalPath,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
onDeleteArtistPress,
|
||||
...otherProps
|
||||
|
@ -156,11 +192,20 @@ class EditArtistModalContent extends Component {
|
|||
|
||||
<SpinnerButton
|
||||
isSpinning={isSaving}
|
||||
onPress={onSavePress}
|
||||
onPress={this.onSavePress}
|
||||
>
|
||||
Save
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
|
||||
<MoveArtistModal
|
||||
originalPath={originalPath}
|
||||
destinationPath={path.value}
|
||||
isOpen={this.state.isConfirmMoveModalOpen}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveArtistPress={this.onMoveArtistPress}
|
||||
/>
|
||||
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
@ -173,6 +218,8 @@ EditArtistModalContent.propTypes = {
|
|||
isSaving: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
isPathChanging: PropTypes.bool.isRequired,
|
||||
originalPath: PropTypes.string.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
|
|
|
@ -37,7 +37,8 @@ function createMapStateToProps() {
|
|||
artistName: artist.artistName,
|
||||
isSaving,
|
||||
saveError,
|
||||
pendingChanges,
|
||||
isPathChanging: pendingChanges.hasOwnProperty('path'),
|
||||
originalPath: artist.path,
|
||||
item: settings.settings,
|
||||
showLanguageProfile: languageProfiles.items.length > 1,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
|
@ -48,8 +49,8 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setArtistValue,
|
||||
saveArtist
|
||||
dispatchSetArtistValue: setArtistValue,
|
||||
dispatchSaveArtist: saveArtist
|
||||
};
|
||||
|
||||
class EditArtistModalContentConnector extends Component {
|
||||
|
@ -67,11 +68,14 @@ class EditArtistModalContentConnector extends Component {
|
|||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.setArtistValue({ name, value });
|
||||
this.props.dispatchSetArtistValue({ name, value });
|
||||
}
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.saveArtist({ id: this.props.artistId });
|
||||
onSavePress = (moveFiles) => {
|
||||
this.props.dispatchSaveArtist({
|
||||
id: this.props.artistId,
|
||||
moveFiles
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -83,6 +87,7 @@ class EditArtistModalContentConnector extends Component {
|
|||
{...this.props}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
onMoveArtistPress={this.onMoveArtistPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -92,8 +97,8 @@ EditArtistModalContentConnector.propTypes = {
|
|||
artistId: PropTypes.number,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
setArtistValue: PropTypes.func.isRequired,
|
||||
saveArtist: PropTypes.func.isRequired,
|
||||
dispatchSetArtistValue: PropTypes.func.isRequired,
|
||||
dispatchSaveArtist: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import createClientSideCollectionSelector from 'Store/Selectors/createClientSide
|
|||
import createCommandSelector from 'Store/Selectors/createCommandSelector';
|
||||
import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions';
|
||||
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import ArtistEditor from './ArtistEditor';
|
||||
|
||||
|
@ -27,10 +28,11 @@ function createMapStateToProps() {
|
|||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
setArtistEditorSort,
|
||||
setArtistEditorFilter,
|
||||
saveArtistEditor,
|
||||
fetchRootFolders
|
||||
dispatchSetArtistEditorSort: setArtistEditorSort,
|
||||
dispatchSetArtistEditorFilter: setArtistEditorFilter,
|
||||
dispatchSaveArtistEditor: saveArtistEditor,
|
||||
dispatchFetchRootFolders: fetchRootFolders,
|
||||
dispatchExecuteCommand: executeCommand
|
||||
};
|
||||
|
||||
class ArtistEditorConnector extends Component {
|
||||
|
@ -39,22 +41,29 @@ class ArtistEditorConnector extends Component {
|
|||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchRootFolders();
|
||||
this.props.dispatchFetchRootFolders();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSortPress = (sortKey) => {
|
||||
this.props.setArtistEditorSort({ sortKey });
|
||||
this.props.dispatchSetArtistEditorSort({ sortKey });
|
||||
}
|
||||
|
||||
onFilterSelect = (filterKey, filterValue, filterType) => {
|
||||
this.props.setArtistEditorFilter({ filterKey, filterValue, filterType });
|
||||
this.props.dispatchSetArtistEditorFilter({ filterKey, filterValue, filterType });
|
||||
}
|
||||
|
||||
onSaveSelected = (payload) => {
|
||||
this.props.saveArtistEditor(payload);
|
||||
this.props.dispatchSaveArtistEditor(payload);
|
||||
}
|
||||
|
||||
onMoveSelected = (payload) => {
|
||||
this.props.dispatchExecuteCommand({
|
||||
name: commandNames.MOVE_ARTIST,
|
||||
...payload
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -73,10 +82,11 @@ class ArtistEditorConnector extends Component {
|
|||
}
|
||||
|
||||
ArtistEditorConnector.propTypes = {
|
||||
setArtistEditorSort: PropTypes.func.isRequired,
|
||||
setArtistEditorFilter: PropTypes.func.isRequired,
|
||||
saveArtistEditor: PropTypes.func.isRequired,
|
||||
fetchRootFolders: PropTypes.func.isRequired
|
||||
dispatchSetArtistEditorSort: PropTypes.func.isRequired,
|
||||
dispatchSetArtistEditorFilter: PropTypes.func.isRequired,
|
||||
dispatchSaveArtistEditor: PropTypes.func.isRequired,
|
||||
dispatchFetchRootFolders: PropTypes.func.isRequired,
|
||||
dispatchExecuteCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connectSection(
|
||||
|
|
|
@ -8,6 +8,7 @@ import QualityProfileSelectInputConnector from 'Components/Form/QualityProfileSe
|
|||
import RootFolderSelectInputConnector from 'Components/Form/RootFolderSelectInputConnector';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import PageContentFooter from 'Components/Page/PageContentFooter';
|
||||
import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal';
|
||||
import TagsModal from './Tags/TagsModal';
|
||||
import DeleteArtistModal from './Delete/DeleteArtistModal';
|
||||
import ArtistEditorFooterLabel from './ArtistEditorFooterLabel';
|
||||
|
@ -32,7 +33,9 @@ class ArtistEditorFooter extends Component {
|
|||
rootFolderPath: NO_CHANGE,
|
||||
savingTags: false,
|
||||
isDeleteArtistModalOpen: false,
|
||||
isTagsModalOpen: false
|
||||
isTagsModalOpen: false,
|
||||
isConfirmMoveModalOpen: false,
|
||||
destinationRootFolder: null
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -66,6 +69,12 @@ class ArtistEditorFooter extends Component {
|
|||
}
|
||||
|
||||
switch (name) {
|
||||
case 'rootFolderPath':
|
||||
this.setState({
|
||||
isConfirmMoveModalOpen: true,
|
||||
destinationRootFolder: value
|
||||
});
|
||||
break;
|
||||
case 'monitored':
|
||||
this.props.onSaveSelected({ [name]: value === 'monitored' });
|
||||
break;
|
||||
|
@ -105,6 +114,27 @@ class ArtistEditorFooter extends Component {
|
|||
this.setState({ isTagsModalOpen: false });
|
||||
}
|
||||
|
||||
onSaveRootFolderPress = () => {
|
||||
this.setState({
|
||||
isConfirmMoveModalOpen: false,
|
||||
destinationRootFolder: null
|
||||
});
|
||||
|
||||
this.props.onSaveSelected({ rootFolderPath: this.state.destinationRootFolder });
|
||||
}
|
||||
|
||||
onMoveArtistPress = () => {
|
||||
this.setState({
|
||||
isConfirmMoveModalOpen: false,
|
||||
destinationRootFolder: null
|
||||
});
|
||||
|
||||
this.props.onSaveSelected({
|
||||
rootFolderPath: this.state.destinationRootFolder,
|
||||
moveFiles: true
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -129,7 +159,9 @@ class ArtistEditorFooter extends Component {
|
|||
rootFolderPath,
|
||||
savingTags,
|
||||
isTagsModalOpen,
|
||||
isDeleteArtistModalOpen
|
||||
isDeleteArtistModalOpen,
|
||||
isConfirmMoveModalOpen,
|
||||
destinationRootFolder
|
||||
} = this.state;
|
||||
|
||||
const monitoredOptions = [
|
||||
|
@ -297,6 +329,14 @@ class ArtistEditorFooter extends Component {
|
|||
artistIds={artistIds}
|
||||
onModalClose={this.onDeleteArtistModalClose}
|
||||
/>
|
||||
|
||||
<MoveArtistModal
|
||||
destinationRootFolder={destinationRootFolder}
|
||||
isOpen={isConfirmMoveModalOpen}
|
||||
onSavePress={this.onSaveRootFolderPress}
|
||||
onMoveArtistPress={this.onMoveArtistPress}
|
||||
/>
|
||||
|
||||
</PageContentFooter>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -25,6 +25,11 @@ const columns = [
|
|||
label: 'Source Title',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
label: 'Language',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
|
|
|
@ -8,6 +8,7 @@ import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellCo
|
|||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeLanguage from 'Album/EpisodeLanguage';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
|
@ -61,6 +62,8 @@ class ArtistHistoryRow extends Component {
|
|||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
language,
|
||||
languageCutoffNotMet,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
date,
|
||||
|
@ -89,6 +92,13 @@ class ArtistHistoryRow extends Component {
|
|||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeLanguage
|
||||
language={language}
|
||||
isCutoffNotMet={languageCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
|
@ -147,6 +157,8 @@ ArtistHistoryRow.propTypes = {
|
|||
id: PropTypes.number.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
language: PropTypes.object.isRequired,
|
||||
languageCutoffNotMet: PropTypes.bool.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
|
|
|
@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createQualityProfileSelector from 'Store/Selectors/createQualityProfileSelector';
|
||||
import createLanguageProfileSelector from 'Store/Selectors/createLanguageProfileSelector';
|
||||
|
@ -13,21 +14,21 @@ import * as commandNames from 'Commands/commandNames';
|
|||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { id }) => id,
|
||||
(state, { albums }) => albums,
|
||||
createArtistSelector(),
|
||||
createQualityProfileSelector(),
|
||||
createLanguageProfileSelector(),
|
||||
createMetadataProfileSelector(),
|
||||
createCommandsSelector(),
|
||||
(artistId, albums, qualityProfile, languageProfile, metadataProfile, commands) => {
|
||||
(artist, qualityProfile, languageProfile, metadataProfile, commands) => {
|
||||
const isRefreshingArtist = _.some(commands, (command) => {
|
||||
return command.name === commandNames.REFRESH_ARTIST &&
|
||||
command.body.artistId === artistId;
|
||||
command.body.artistId === artist.id;
|
||||
});
|
||||
|
||||
const latestAlbum = _.first(_.orderBy(albums, 'releaseDate', 'desc'));
|
||||
const latestAlbum = _.maxBy(artist.albums, (album) => album.releaseDate);
|
||||
|
||||
return {
|
||||
...artist,
|
||||
qualityProfile,
|
||||
languageProfile,
|
||||
metadataProfile,
|
||||
|
|
|
@ -31,8 +31,7 @@ $hoverScale: 1.05;
|
|||
}
|
||||
|
||||
.nextAiring {
|
||||
background-color: $defaultColor;
|
||||
color: $white;
|
||||
background-color: #fafbfc;
|
||||
text-align: center;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ class ArtistIndexBanner extends Component {
|
|||
bannerHeight,
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
qualityProfile,
|
||||
showRelativeDates,
|
||||
|
@ -151,6 +152,13 @@ class ArtistIndexBanner extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showMonitored &&
|
||||
<div className={styles.title}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
|
@ -214,6 +222,7 @@ ArtistIndexBanner.propTypes = {
|
|||
bannerHeight: PropTypes.number.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -39,6 +39,7 @@ function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions)
|
|||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = bannerOptions;
|
||||
|
||||
|
@ -55,6 +56,10 @@ function calculateRowHeight(bannerHeight, sortKey, isSmallScreen, bannerOptions)
|
|||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showMonitored) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
@ -213,6 +218,7 @@ class ArtistIndexBanners extends Component {
|
|||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = bannerOptions;
|
||||
|
||||
|
@ -231,12 +237,16 @@ class ArtistIndexBanners extends Component {
|
|||
bannerHeight={bannerHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
showTitle={showTitle}
|
||||
showMonitored={showMonitored}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
style={style}
|
||||
{...artist}
|
||||
artistId={artist.id}
|
||||
languageProfileId={artist.languageProfileId}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showTitle: props.showTitle,
|
||||
showMonitored: props.showMonitored,
|
||||
showQualityProfile: props.showQualityProfile
|
||||
};
|
||||
}
|
||||
|
@ -39,6 +40,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = this.props;
|
||||
|
||||
|
@ -56,6 +58,10 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
state.showTitle = showTitle;
|
||||
}
|
||||
|
||||
if (showMonitored !== prevProps.showMonitored) {
|
||||
state.showMonitored = showMonitored;
|
||||
}
|
||||
|
||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
||||
state.showQualityProfile = showQualityProfile;
|
||||
}
|
||||
|
@ -68,11 +74,11 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
//
|
||||
// Listeners
|
||||
|
||||
onChangeOption = ({ name, value }) => {
|
||||
onChangeBannerOption = ({ name, value }) => {
|
||||
this.setState({
|
||||
[name]: value
|
||||
}, () => {
|
||||
this.props.onChangeOption({ [name]: value });
|
||||
this.props.onChangeBannerOption({ [name]: value });
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -88,6 +94,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = this.state;
|
||||
|
||||
|
@ -107,7 +114,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
name="size"
|
||||
value={size}
|
||||
values={bannerSizeOptions}
|
||||
onChange={this.onChangeOption}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -119,7 +126,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
name="detailedProgressBar"
|
||||
value={detailedProgressBar}
|
||||
helpText="Show text on progess bar"
|
||||
onChange={this.onChangeOption}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -131,7 +138,19 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
name="showTitle"
|
||||
value={showTitle}
|
||||
helpText="Show artist name under banner"
|
||||
onChange={this.onChangeOption}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Show Monitored</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
helpText="Show monitored status under banner"
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
@ -143,7 +162,7 @@ class ArtistIndexBannerOptionsModalContent extends Component {
|
|||
name="showQualityProfile"
|
||||
value={showQualityProfile}
|
||||
helpText="Show quality profile under banner"
|
||||
onChange={this.onChangeOption}
|
||||
onChange={this.onChangeBannerOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
|
@ -166,7 +185,8 @@ ArtistIndexBannerOptionsModalContent.propTypes = {
|
|||
showTitle: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
onChangeOption: PropTypes.func.isRequired,
|
||||
onChangeBannerOption: PropTypes.func.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ function createMapStateToProps() {
|
|||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onChangeOption(payload) {
|
||||
onChangeBannerOption(payload) {
|
||||
dispatch(setArtistBannerOption(payload));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,6 +5,7 @@ import { icons } from 'Helpers/Props';
|
|||
import dimensions from 'Styles/Variables/dimensions';
|
||||
import fonts from 'Styles/Variables/fonts';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
|
@ -188,6 +189,7 @@ class ArtistIndexOverview extends Component {
|
|||
|
||||
<ArtistIndexOverviewInfo
|
||||
height={overviewHeight}
|
||||
monitored={monitored}
|
||||
nextAiring={nextAiring}
|
||||
qualityProfile={qualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
|
|
|
@ -21,11 +21,13 @@ function isVisible(name, show, value, sortKey, index) {
|
|||
function ArtistIndexOverviewInfo(props) {
|
||||
const {
|
||||
height,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showAdded,
|
||||
showAlbumCount,
|
||||
showPath,
|
||||
showSizeOnDisk,
|
||||
monitored,
|
||||
nextAiring,
|
||||
qualityProfile,
|
||||
added,
|
||||
|
@ -47,6 +49,7 @@ function ArtistIndexOverviewInfo(props) {
|
|||
}
|
||||
|
||||
const maxRows = Math.floor(height / (infoRowHeight + 4));
|
||||
const monitoredText = monitored ? 'Monitored' : 'Unmonitored';
|
||||
|
||||
return (
|
||||
<div className={styles.infos}>
|
||||
|
@ -77,7 +80,23 @@ function ArtistIndexOverviewInfo(props) {
|
|||
}
|
||||
|
||||
{
|
||||
isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 1 &&
|
||||
isVisible('monitored', showMonitored, monitored, sortKey) && maxRows > 1 &&
|
||||
<div
|
||||
className={styles.info}
|
||||
title={monitoredText}
|
||||
>
|
||||
<Icon
|
||||
className={styles.icon}
|
||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||
size={14}
|
||||
/>
|
||||
|
||||
{monitoredText}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isVisible('qualityProfileId', showQualityProfile, qualityProfile, sortKey) && maxRows > 2 &&
|
||||
<div
|
||||
className={styles.info}
|
||||
title="Quality Profile"
|
||||
|
@ -93,7 +112,7 @@ function ArtistIndexOverviewInfo(props) {
|
|||
}
|
||||
|
||||
{
|
||||
isVisible('added', showAdded, added, sortKey) && maxRows > 2 &&
|
||||
isVisible('added', showAdded, added, sortKey) && maxRows > 3 &&
|
||||
<div
|
||||
className={styles.info}
|
||||
title="Date Added"
|
||||
|
@ -119,7 +138,7 @@ function ArtistIndexOverviewInfo(props) {
|
|||
}
|
||||
|
||||
{
|
||||
isVisible('albumCount', showAlbumCount, albumCount, sortKey) && maxRows > 3 &&
|
||||
isVisible('albumCount', showAlbumCount, albumCount, sortKey) && maxRows > 4 &&
|
||||
<div
|
||||
className={styles.info}
|
||||
title="Album Count"
|
||||
|
@ -135,7 +154,7 @@ function ArtistIndexOverviewInfo(props) {
|
|||
}
|
||||
|
||||
{
|
||||
isVisible('path', showPath, path, sortKey) && maxRows > 4 &&
|
||||
isVisible('path', showPath, path, sortKey) && maxRows > 5 &&
|
||||
<div
|
||||
className={styles.info}
|
||||
title="Path"
|
||||
|
@ -151,7 +170,7 @@ function ArtistIndexOverviewInfo(props) {
|
|||
}
|
||||
|
||||
{
|
||||
isVisible('sizeOnDisk', showSizeOnDisk, sizeOnDisk, sortKey) && maxRows > 5 &&
|
||||
isVisible('sizeOnDisk', showSizeOnDisk, sizeOnDisk, sortKey) && maxRows > 6 &&
|
||||
<div
|
||||
className={styles.info}
|
||||
title="Size on Disk"
|
||||
|
@ -173,11 +192,13 @@ function ArtistIndexOverviewInfo(props) {
|
|||
ArtistIndexOverviewInfo.propTypes = {
|
||||
height: PropTypes.number.isRequired,
|
||||
showNetwork: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
showAdded: PropTypes.bool.isRequired,
|
||||
showAlbumCount: PropTypes.bool.isRequired,
|
||||
showPath: PropTypes.bool.isRequired,
|
||||
showSizeOnDisk: PropTypes.bool.isRequired,
|
||||
monitored: PropTypes.bool.isRequired,
|
||||
nextAiring: PropTypes.string,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
previousAiring: PropTypes.string,
|
||||
|
|
|
@ -191,7 +191,10 @@ class ArtistIndexOverviews extends Component {
|
|||
timeFormat={timeFormat}
|
||||
isSmallScreen={isSmallScreen}
|
||||
style={style}
|
||||
{...artist}
|
||||
artistId={artist.id}
|
||||
languageProfileId={artist.languageProfileId}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
|
|||
this.state = {
|
||||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showMonitored: props.showMonitored,
|
||||
showQualityProfile: props.showQualityProfile,
|
||||
showPreviousAiring: props.showPreviousAiring,
|
||||
showAdded: props.showAdded,
|
||||
|
@ -42,6 +43,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
|
|||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showPreviousAiring,
|
||||
showAdded,
|
||||
|
@ -60,6 +62,10 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
|
|||
state.size = size;
|
||||
}
|
||||
|
||||
if (showMonitored !== prevProps.showMonitored) {
|
||||
state.showMonitored = showMonitored;
|
||||
}
|
||||
|
||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
||||
state.showQualityProfile = showQualityProfile;
|
||||
}
|
||||
|
@ -111,6 +117,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
|
|||
const {
|
||||
detailedProgressBar,
|
||||
size,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
showPreviousAiring,
|
||||
showAdded,
|
||||
|
@ -152,6 +159,18 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
|
|||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Show Monitored</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
onChange={this.onChangeOverviewOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
|
||||
<FormLabel>Show Quality Profile</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
|
@ -233,6 +252,7 @@ class ArtistIndexOverviewOptionsModalContent extends Component {
|
|||
|
||||
ArtistIndexOverviewOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
showPreviousAiring: PropTypes.bool.isRequired,
|
||||
showAdded: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -31,8 +31,7 @@ $hoverScale: 1.05;
|
|||
}
|
||||
|
||||
.nextAiring {
|
||||
background-color: $defaultColor;
|
||||
color: $white;
|
||||
background-color: #fafbfc;
|
||||
text-align: center;
|
||||
font-size: $smallFontSize;
|
||||
}
|
||||
|
|
|
@ -68,6 +68,7 @@ class ArtistIndexPoster extends Component {
|
|||
posterHeight,
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile,
|
||||
qualityProfile,
|
||||
showRelativeDates,
|
||||
|
@ -151,6 +152,13 @@ class ArtistIndexPoster extends Component {
|
|||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showMonitored &&
|
||||
<div className={styles.title}>
|
||||
{monitored ? 'Monitored' : 'Unmonitored'}
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
showQualityProfile &&
|
||||
<div className={styles.title}>
|
||||
|
@ -214,6 +222,7 @@ ArtistIndexPoster.propTypes = {
|
|||
posterHeight: PropTypes.number.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
qualityProfile: PropTypes.object.isRequired,
|
||||
showRelativeDates: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -39,6 +39,7 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = posterOptions;
|
||||
|
||||
|
@ -55,6 +56,10 @@ function calculateRowHeight(posterHeight, sortKey, isSmallScreen, posterOptions)
|
|||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showMonitored) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
||||
if (showQualityProfile) {
|
||||
heights.push(19);
|
||||
}
|
||||
|
@ -213,6 +218,7 @@ class ArtistIndexPosters extends Component {
|
|||
const {
|
||||
detailedProgressBar,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = posterOptions;
|
||||
|
||||
|
@ -231,12 +237,16 @@ class ArtistIndexPosters extends Component {
|
|||
posterHeight={posterHeight}
|
||||
detailedProgressBar={detailedProgressBar}
|
||||
showTitle={showTitle}
|
||||
showMonitored={showMonitored}
|
||||
showQualityProfile={showQualityProfile}
|
||||
showRelativeDates={showRelativeDates}
|
||||
shortDateFormat={shortDateFormat}
|
||||
timeFormat={timeFormat}
|
||||
style={style}
|
||||
{...artist}
|
||||
artistId={artist.id}
|
||||
languageProfileId={artist.languageProfileId}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
|
|||
detailedProgressBar: props.detailedProgressBar,
|
||||
size: props.size,
|
||||
showTitle: props.showTitle,
|
||||
showMonitored: props.showMonitored,
|
||||
showQualityProfile: props.showQualityProfile
|
||||
};
|
||||
}
|
||||
|
@ -39,6 +40,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
|
|||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = this.props;
|
||||
|
||||
|
@ -56,6 +58,10 @@ class ArtistIndexPosterOptionsModalContent extends Component {
|
|||
state.showTitle = showTitle;
|
||||
}
|
||||
|
||||
if (showMonitored !== prevProps.showMonitored) {
|
||||
state.showMonitored = showMonitored;
|
||||
}
|
||||
|
||||
if (showQualityProfile !== prevProps.showQualityProfile) {
|
||||
state.showQualityProfile = showQualityProfile;
|
||||
}
|
||||
|
@ -88,6 +94,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
|
|||
detailedProgressBar,
|
||||
size,
|
||||
showTitle,
|
||||
showMonitored,
|
||||
showQualityProfile
|
||||
} = this.state;
|
||||
|
||||
|
@ -135,6 +142,18 @@ class ArtistIndexPosterOptionsModalContent extends Component {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Show Monitored</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="showMonitored"
|
||||
value={showMonitored}
|
||||
helpText="Show monitored status under poster"
|
||||
onChange={this.onChangePosterOption}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Show Quality Profile</FormLabel>
|
||||
|
||||
|
@ -164,6 +183,7 @@ class ArtistIndexPosterOptionsModalContent extends Component {
|
|||
ArtistIndexPosterOptionsModalContent.propTypes = {
|
||||
size: PropTypes.string.isRequired,
|
||||
showTitle: PropTypes.bool.isRequired,
|
||||
showMonitored: PropTypes.bool.isRequired,
|
||||
showQualityProfile: PropTypes.bool.isRequired,
|
||||
detailedProgressBar: PropTypes.bool.isRequired,
|
||||
onChangePosterOption: PropTypes.func.isRequired,
|
||||
|
|
|
@ -10,34 +10,6 @@ import styles from './ArtistIndexTable.css';
|
|||
|
||||
class ArtistIndexTable extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._table = null;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
columns,
|
||||
filterKey,
|
||||
filterValue,
|
||||
sortKey,
|
||||
sortDirection
|
||||
} = this.props;
|
||||
|
||||
if (prevProps.columns !== columns ||
|
||||
prevProps.filterKey !== filterKey ||
|
||||
prevProps.filterValue !== filterValue ||
|
||||
prevProps.sortKey !== sortKey ||
|
||||
prevProps.sortDirection !== sortDirection
|
||||
) {
|
||||
this._table.forceUpdateGrid();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
|
@ -59,10 +31,6 @@ class ArtistIndexTable extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
setTableRef = (ref) => {
|
||||
this._table = ref;
|
||||
}
|
||||
|
||||
rowRenderer = ({ key, rowIndex, style }) => {
|
||||
const {
|
||||
items,
|
||||
|
@ -77,7 +45,10 @@ class ArtistIndexTable extends Component {
|
|||
component={ArtistIndexRow}
|
||||
style={style}
|
||||
columns={columns}
|
||||
{...artist}
|
||||
artistId={artist.id}
|
||||
languageProfileId={artist.languageProfileId}
|
||||
qualityProfileId={artist.qualityProfileId}
|
||||
metadataProfileId={artist.metadataProfileId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -89,6 +60,8 @@ class ArtistIndexTable extends Component {
|
|||
const {
|
||||
items,
|
||||
columns,
|
||||
filterKey,
|
||||
filterValue,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
isSmallScreen,
|
||||
|
@ -101,7 +74,6 @@ class ArtistIndexTable extends Component {
|
|||
|
||||
return (
|
||||
<VirtualTable
|
||||
ref={this.setTableRef}
|
||||
className={styles.tableContainer}
|
||||
items={items}
|
||||
scrollTop={scrollTop}
|
||||
|
@ -118,6 +90,11 @@ class ArtistIndexTable extends Component {
|
|||
onSortPress={onSortPress}
|
||||
/>
|
||||
}
|
||||
columns={columns}
|
||||
filterKey={filterKey}
|
||||
filterValue={filterValue}
|
||||
sortKey={sortKey}
|
||||
sortDirection={sortDirection}
|
||||
onRender={onRender}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
|
|
5
frontend/src/Artist/MoveArtist/MoveArtistModal.css
Normal file
5
frontend/src/Artist/MoveArtist/MoveArtistModal.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.doNotMoveButton {
|
||||
composes: button from 'Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
83
frontend/src/Artist/MoveArtist/MoveArtistModal.js
Normal file
83
frontend/src/Artist/MoveArtist/MoveArtistModal.js
Normal file
|
@ -0,0 +1,83 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './MoveArtistModal.css';
|
||||
|
||||
function MoveArtistModal(props) {
|
||||
const {
|
||||
originalPath,
|
||||
destinationPath,
|
||||
destinationRootFolder,
|
||||
isOpen,
|
||||
onSavePress,
|
||||
onMoveArtistPress
|
||||
} = props;
|
||||
|
||||
if (
|
||||
isOpen &&
|
||||
!originalPath &&
|
||||
!destinationPath &&
|
||||
!destinationRootFolder
|
||||
) {
|
||||
console.error('orginalPath and destinationPath OR destinationRootFolder must be provied');
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.MEDIUM}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onSavePress}
|
||||
>
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onSavePress}
|
||||
>
|
||||
<ModalHeader>
|
||||
Move Files
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
destinationRootFolder ?
|
||||
`Would you like to move the artist folders to ${destinationPath}'?` :
|
||||
`Would you like to move the artist files from '${originalPath}' to '${destinationPath}'?`
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
className={styles.doNotMoveButton}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
No, I'll Move the Files Myself
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onMoveArtistPress}
|
||||
>
|
||||
Yes, Move the Files
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
MoveArtistModal.propTypes = {
|
||||
originalPath: PropTypes.string,
|
||||
destinationPath: PropTypes.string,
|
||||
destinationRootFolder: PropTypes.string,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
onMoveArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default MoveArtistModal;
|
|
@ -10,6 +10,7 @@ export const DOWNLOADED_ALBUMS_SCAN = 'DownloadedAlbumsScan';
|
|||
export const ALBUM_SEARCH = 'AlbumSearch';
|
||||
export const INTERACTIVE_IMPORT = 'ManualImport';
|
||||
export const MISSING_ALBUM_SEARCH = 'MissingAlbumSearch';
|
||||
export const MOVE_ARTIST = 'MoveArtist';
|
||||
export const REFRESH_ARTIST = 'RefreshArtist';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_ARTIST = 'RenameArtist';
|
||||
|
|
|
@ -31,12 +31,19 @@
|
|||
.isDisabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
pointer-events: all !important;
|
||||
}
|
||||
|
||||
.dropdownArrowContainer {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.dropdownArrowContainerDisabled {
|
||||
composes: dropdownArrowContainer;
|
||||
|
||||
color: $disabledInputColor;
|
||||
}
|
||||
|
||||
.optionsContainer {
|
||||
width: auto;
|
||||
}
|
||||
|
|
|
@ -289,6 +289,7 @@ class EnhancedSelectInput extends Component {
|
|||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={this.onBlur}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onPress={this.onPress}
|
||||
|
@ -296,11 +297,17 @@ class EnhancedSelectInput extends Component {
|
|||
<SelectedValueComponent
|
||||
{...selectedValueOptions}
|
||||
{...selectedOption}
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : null}
|
||||
</SelectedValueComponent>
|
||||
|
||||
<div className={styles.dropdownArrowContainer}>
|
||||
<div
|
||||
className={isDisabled ?
|
||||
styles.dropdownArrowContainerDisabled :
|
||||
styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CARET_DOWN}
|
||||
/>
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.selectedValue {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.isDisabled {
|
||||
color: $disabledInputColor;
|
||||
}
|
||||
|
|
|
@ -1,15 +1,21 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import styles from './EnhancedSelectInputSelectedValue.css';
|
||||
|
||||
function EnhancedSelectInputSelectedValue(props) {
|
||||
const {
|
||||
className,
|
||||
children
|
||||
children,
|
||||
isDisabled
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={classNames(
|
||||
className,
|
||||
isDisabled && styles.isDisabled
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -17,11 +23,13 @@ function EnhancedSelectInputSelectedValue(props) {
|
|||
|
||||
EnhancedSelectInputSelectedValue.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
children: PropTypes.node
|
||||
children: PropTypes.node,
|
||||
isDisabled: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
EnhancedSelectInputSelectedValue.defaultProps = {
|
||||
className: styles.selectedValue
|
||||
className: styles.selectedValue,
|
||||
isDisabled: false
|
||||
};
|
||||
|
||||
export default EnhancedSelectInputSelectedValue;
|
||||
|
|
|
@ -109,8 +109,17 @@ class Modal extends Component {
|
|||
}
|
||||
|
||||
onBackdropEndPress = (event) => {
|
||||
if (this._isBackdropPressed && this._isBackdropTarget(event)) {
|
||||
this.props.onModalClose();
|
||||
const {
|
||||
closeOnBackgroundClick,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
this._isBackdropPressed &&
|
||||
this._isBackdropTarget(event) &&
|
||||
closeOnBackgroundClick
|
||||
) {
|
||||
onModalClose();
|
||||
}
|
||||
|
||||
this._isBackdropPressed = false;
|
||||
|
@ -187,13 +196,15 @@ Modal.propTypes = {
|
|||
size: PropTypes.oneOf(sizes.all),
|
||||
children: PropTypes.node,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
closeOnBackgroundClick: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
Modal.defaultProps = {
|
||||
className: styles.modal,
|
||||
backdropClassName: styles.modalBackdrop,
|
||||
size: sizes.LARGE
|
||||
size: sizes.LARGE,
|
||||
closeOnBackgroundClick: true
|
||||
};
|
||||
|
||||
export default Modal;
|
||||
|
|
|
@ -9,6 +9,7 @@ function ModalContent(props) {
|
|||
const {
|
||||
className,
|
||||
children,
|
||||
showCloseButton,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
@ -18,15 +19,18 @@ function ModalContent(props) {
|
|||
className={className}
|
||||
{...otherProps}
|
||||
>
|
||||
<Link
|
||||
className={styles.closeButton}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CLOSE}
|
||||
size={18}
|
||||
/>
|
||||
</Link>
|
||||
{
|
||||
showCloseButton &&
|
||||
<Link
|
||||
className={styles.closeButton}
|
||||
onPress={onModalClose}
|
||||
>
|
||||
<Icon
|
||||
name={icons.CLOSE}
|
||||
size={18}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
|
@ -36,11 +40,13 @@ function ModalContent(props) {
|
|||
ModalContent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
children: PropTypes.node,
|
||||
showCloseButton: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ModalContent.defaultProps = {
|
||||
className: styles.modalContent
|
||||
className: styles.modalContent,
|
||||
showCloseButton: true
|
||||
};
|
||||
|
||||
export default ModalContent;
|
||||
|
|
|
@ -482,7 +482,7 @@ class PageSidebar extends Component {
|
|||
key={child.to}
|
||||
title={child.title}
|
||||
to={child.to}
|
||||
isActive={pathname === child.to}
|
||||
isActive={pathname.startsWith(child.to)}
|
||||
isParentItem={false}
|
||||
isChildItem={true}
|
||||
statusComponent={child.statusComponent}
|
||||
|
|
|
@ -44,7 +44,6 @@ class VirtualTable extends Component {
|
|||
};
|
||||
|
||||
this._isInitialized = false;
|
||||
this._table = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -58,18 +57,9 @@ class VirtualTable extends Component {
|
|||
return this.props.items[index];
|
||||
}
|
||||
|
||||
setTableRef = (ref) => {
|
||||
this._table = ref;
|
||||
}
|
||||
|
||||
forceUpdateGrid = () => {
|
||||
this._table.recomputeGridSize();
|
||||
}
|
||||
|
||||
scrollToRow = (rowIndex) => {
|
||||
const scrollTop = (rowIndex + 1) * ROW_HEIGHT + 20;
|
||||
|
||||
// this._table.scrollToCell({ columnIndex: 0, rowIndex });
|
||||
this.props.onScroll({ scrollTop });
|
||||
}
|
||||
|
||||
|
@ -124,7 +114,6 @@ class VirtualTable extends Component {
|
|||
{header}
|
||||
|
||||
<VirtualTableBody
|
||||
ref={this.setTableRef}
|
||||
autoContainerWidth={true}
|
||||
width={width}
|
||||
height={height}
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
.filterContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filterText {
|
||||
margin-left: 5px;
|
||||
font-size: $largeFontSize;
|
||||
}
|
||||
|
||||
.footer {
|
||||
composes: modalFooter from 'Components/Modal/ModalFooter.css';
|
||||
|
||||
|
|
|
@ -4,11 +4,15 @@ import React, { Component } from 'react';
|
|||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import SelectInput from 'Components/Form/SelectInput';
|
||||
import Menu from 'Components/Menu/Menu';
|
||||
import MenuButton from 'Components/Menu/MenuButton';
|
||||
import MenuContent from 'Components/Menu/MenuContent';
|
||||
import SelectedMenuItem from 'Components/Menu/SelectedMenuItem';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
|
@ -70,6 +74,11 @@ const columns = [
|
|||
}
|
||||
];
|
||||
|
||||
const filterExistingFilesOptions = {
|
||||
ALL: 'all',
|
||||
NEW: 'new'
|
||||
};
|
||||
|
||||
class InteractiveImportModalContent extends Component {
|
||||
|
||||
//
|
||||
|
@ -129,6 +138,10 @@ class InteractiveImportModalContent extends Component {
|
|||
this.props.onImportSelectedPress(selected, this.state.importMode);
|
||||
}
|
||||
|
||||
onFilterExistingFilesChange = (value) => {
|
||||
this.props.onFilterExistingFilesChange(value !== filterExistingFilesOptions.ALL);
|
||||
}
|
||||
|
||||
onImportModeChange = ({ value }) => {
|
||||
this.props.onImportModeChange(value);
|
||||
}
|
||||
|
@ -155,6 +168,8 @@ class InteractiveImportModalContent extends Component {
|
|||
render() {
|
||||
const {
|
||||
downloadId,
|
||||
showFilterExistingFiles,
|
||||
filterExistingFiles,
|
||||
title,
|
||||
folder,
|
||||
isFetching,
|
||||
|
@ -205,7 +220,45 @@ class InteractiveImportModalContent extends Component {
|
|||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
isPopulated && showFilterExistingFiles && !isFetching &&
|
||||
<div className={styles.filterContainer}>
|
||||
<Menu alignMenu={align.RIGHT}>
|
||||
<MenuButton>
|
||||
<Icon
|
||||
name={icons.FILTER}
|
||||
size={22}
|
||||
/>
|
||||
|
||||
<div className={styles.filterText}>
|
||||
{
|
||||
filterExistingFiles ? 'Unmapped Files Only' : 'All Files'
|
||||
}
|
||||
</div>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent>
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.ALL}
|
||||
isSelected={!filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
All Files
|
||||
</SelectedMenuItem>
|
||||
|
||||
<SelectedMenuItem
|
||||
name={filterExistingFilesOptions.NEW}
|
||||
isSelected={filterExistingFiles}
|
||||
onPress={this.onFilterExistingFilesChange}
|
||||
>
|
||||
Unmapped Files Only
|
||||
</SelectedMenuItem>
|
||||
</MenuContent>
|
||||
</Menu>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !!items.length && !isFetching && !isFetching &&
|
||||
<Table
|
||||
columns={columns}
|
||||
selectAll={true}
|
||||
|
@ -235,7 +288,7 @@ class InteractiveImportModalContent extends Component {
|
|||
}
|
||||
|
||||
{
|
||||
isPopulated && !items.length &&
|
||||
isPopulated && !items.length && !isFetching &&
|
||||
'No audio files were found in the selected folder'
|
||||
}
|
||||
</ModalBody>
|
||||
|
@ -303,6 +356,8 @@ class InteractiveImportModalContent extends Component {
|
|||
|
||||
InteractiveImportModalContent.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
showFilterExistingFiles: PropTypes.bool.isRequired,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
importMode: PropTypes.string.isRequired,
|
||||
title: PropTypes.string,
|
||||
folder: PropTypes.string,
|
||||
|
@ -314,12 +369,14 @@ InteractiveImportModalContent.propTypes = {
|
|||
sortDirection: PropTypes.string,
|
||||
interactiveImportErrorMessage: PropTypes.string,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
onFilterExistingFilesChange: PropTypes.func.isRequired,
|
||||
onImportModeChange: PropTypes.func.isRequired,
|
||||
onImportSelectedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModalContent.defaultProps = {
|
||||
showFilterExistingFiles: false,
|
||||
importMode: 'move'
|
||||
};
|
||||
|
||||
|
|
|
@ -35,7 +35,8 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
interactiveImportErrorMessage: null
|
||||
interactiveImportErrorMessage: null,
|
||||
filterExistingFiles: true
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -45,7 +46,34 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
folder
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchInteractiveImportItems({ downloadId, folder });
|
||||
const {
|
||||
filterExistingFiles
|
||||
} = this.state;
|
||||
|
||||
this.props.fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
filterExistingFiles
|
||||
} = this.state;
|
||||
|
||||
if (prevState.filterExistingFiles !== filterExistingFiles) {
|
||||
const {
|
||||
downloadId,
|
||||
folder
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchInteractiveImportItems({
|
||||
downloadId,
|
||||
folder,
|
||||
filterExistingFiles
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -59,6 +87,10 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
this.props.setInteractiveImportSort({ sortKey, sortDirection });
|
||||
}
|
||||
|
||||
onFilterExistingFilesChange = (filterExistingFiles) => {
|
||||
this.setState({ filterExistingFiles });
|
||||
}
|
||||
|
||||
onImportModeChange = (importMode) => {
|
||||
this.props.setInteractiveImportMode({ importMode });
|
||||
}
|
||||
|
@ -122,11 +154,18 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
interactiveImportErrorMessage,
|
||||
filterExistingFiles
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<InteractiveImportModalContent
|
||||
{...this.props}
|
||||
interactiveImportErrorMessage={this.state.interactiveImportErrorMessage}
|
||||
interactiveImportErrorMessage={interactiveImportErrorMessage}
|
||||
filterExistingFiles={filterExistingFiles}
|
||||
onSortPress={this.onSortPress}
|
||||
onFilterExistingFilesChange={this.onFilterExistingFilesChange}
|
||||
onImportModeChange={this.onImportModeChange}
|
||||
onImportSelectedPress={this.onImportSelectedPress}
|
||||
/>
|
||||
|
@ -137,6 +176,7 @@ class InteractiveImportModalContentConnector extends Component {
|
|||
InteractiveImportModalContentConnector.propTypes = {
|
||||
downloadId: PropTypes.string,
|
||||
folder: PropTypes.string,
|
||||
filterExistingFiles: PropTypes.bool.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
fetchInteractiveImportItems: PropTypes.func.isRequired,
|
||||
setInteractiveImportSort: PropTypes.func.isRequired,
|
||||
|
@ -146,6 +186,10 @@ InteractiveImportModalContentConnector.propTypes = {
|
|||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
InteractiveImportModalContentConnector.defaultProps = {
|
||||
filterExistingFiles: true
|
||||
};
|
||||
|
||||
export default connectSection(
|
||||
createMapStateToProps,
|
||||
mapDispatchToProps,
|
||||
|
|
|
@ -15,6 +15,12 @@ import TableBody from 'Components/Table/TableBody';
|
|||
import SelectTrackRow from './SelectTrackRow';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'mediumNumber',
|
||||
label: 'Medium',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'trackNumber',
|
||||
label: '#',
|
||||
|
@ -127,7 +133,8 @@ class SelectTrackModalContent extends Component {
|
|||
<SelectTrackRow
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
trackNumber={item.trackNumber}
|
||||
mediumNumber={item.mediumNumber}
|
||||
trackNumber={item.absoluteTrackNumber}
|
||||
title={item.title}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
|
|
|
@ -24,6 +24,7 @@ class SelectTrackRow extends Component {
|
|||
render() {
|
||||
const {
|
||||
id,
|
||||
mediumNumber,
|
||||
trackNumber,
|
||||
title,
|
||||
isSelected,
|
||||
|
@ -38,6 +39,10 @@ class SelectTrackRow extends Component {
|
|||
onSelectedChange={onSelectedChange}
|
||||
/>
|
||||
|
||||
<TableRowCell>
|
||||
{mediumNumber}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{trackNumber}
|
||||
</TableRowCell>
|
||||
|
@ -53,6 +58,7 @@ class SelectTrackRow extends Component {
|
|||
|
||||
SelectTrackRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
mediumNumber: PropTypes.number.isRequired,
|
||||
trackNumber: PropTypes.number.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
|
|
|
@ -264,7 +264,7 @@ class MediaManagement extends Component {
|
|||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>File chmod mask</FormLabel>
|
||||
<FormLabel>File chmod mode</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
|
@ -279,7 +279,7 @@ class MediaManagement extends Component {
|
|||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
>
|
||||
<FormLabel>Folder chmod mask</FormLabel>
|
||||
<FormLabel>Folder chmod mode</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
|
|
|
@ -6,13 +6,13 @@ function createRemoveItemHandler(section, url) {
|
|||
return function(getState, payload, dispatch) {
|
||||
const {
|
||||
id,
|
||||
...queryParms
|
||||
...queryParams
|
||||
} = payload;
|
||||
|
||||
dispatch(set({ section, isDeleting: true }));
|
||||
|
||||
const ajaxOptions = {
|
||||
url: `${url}/${id}?${$.param(queryParms, true)}`,
|
||||
url: `${url}/${id}?${$.param(queryParams, true)}`,
|
||||
method: 'DELETE'
|
||||
};
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import $ from 'jquery';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getProviderState from 'Utilities/State/getProviderState';
|
||||
|
@ -14,15 +15,19 @@ export function createCancelSaveProviderHandler(section) {
|
|||
};
|
||||
}
|
||||
|
||||
function createSaveProviderHandler(section, url) {
|
||||
function createSaveProviderHandler(section, url, options = {}) {
|
||||
return function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isSaving: true }));
|
||||
|
||||
const id = payload.id;
|
||||
const {
|
||||
id,
|
||||
queryParams = {}
|
||||
} = payload;
|
||||
|
||||
const saveData = getProviderState(payload, getState, section);
|
||||
|
||||
const ajaxOptions = {
|
||||
url,
|
||||
url: `${url}?${$.param(queryParams, true)}`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
|
@ -30,7 +35,7 @@ function createSaveProviderHandler(section, url) {
|
|||
};
|
||||
|
||||
if (id) {
|
||||
ajaxOptions.url = `${url}/${id}`;
|
||||
ajaxOptions.url = `${url}/${id}?${$.param(queryParams, true)}`;
|
||||
ajaxOptions.method = 'PUT';
|
||||
}
|
||||
|
||||
|
|
|
@ -46,8 +46,31 @@ export const TOGGLE_ALBUM_MONITORED = 'artist/toggleAlbumMonitored';
|
|||
// Action Creators
|
||||
|
||||
export const fetchArtist = createThunk(FETCH_ARTIST);
|
||||
export const saveArtist = createThunk(SAVE_ARTIST);
|
||||
export const deleteArtist = createThunk(DELETE_ARTIST);
|
||||
export const saveArtist = createThunk(SAVE_ARTIST, (payload) => {
|
||||
const newPayload = {
|
||||
...payload
|
||||
};
|
||||
|
||||
if (payload.moveFiles) {
|
||||
newPayload.queryParams = {
|
||||
moveFiles: true
|
||||
};
|
||||
}
|
||||
|
||||
delete newPayload.moveFiles;
|
||||
|
||||
return newPayload;
|
||||
});
|
||||
|
||||
export const deleteArtist = createThunk(DELETE_ARTIST, (payload) => {
|
||||
return {
|
||||
...payload,
|
||||
queryParams: {
|
||||
deleteFiles: payload.deleteFiles
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
export const toggleArtistMonitored = createThunk(TOGGLE_ARTIST_MONITORED);
|
||||
export const toggleAlbumMonitored = createThunk(TOGGLE_ALBUM_MONITORED);
|
||||
|
||||
|
@ -58,20 +81,25 @@ export const setArtistValue = createAction(SET_ARTIST_VALUE, (payload) => {
|
|||
};
|
||||
});
|
||||
|
||||
//
|
||||
// Helpers
|
||||
|
||||
function getSaveAjaxOptions({ ajaxOptions, payload }) {
|
||||
if (payload.moveFolder) {
|
||||
ajaxOptions.url = `${ajaxOptions.url}?moveFolder=true`;
|
||||
}
|
||||
|
||||
return ajaxOptions;
|
||||
}
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_ARTIST]: createFetchHandler(section, '/artist'),
|
||||
|
||||
[SAVE_ARTIST]: createSaveProviderHandler(
|
||||
section, '/artist'),
|
||||
|
||||
[DELETE_ARTIST]: createRemoveItemHandler(
|
||||
section,
|
||||
'/artist'
|
||||
),
|
||||
[SAVE_ARTIST]: createSaveProviderHandler(section, '/artist', { getAjaxOptions: getSaveAjaxOptions }),
|
||||
[DELETE_ARTIST]: createRemoveItemHandler(section, '/artist'),
|
||||
|
||||
[TOGGLE_ARTIST_MONITORED]: (getState, payload, dispatch) => {
|
||||
const {
|
||||
|
@ -115,7 +143,7 @@ export const actionHandlers = handleThunks({
|
|||
});
|
||||
},
|
||||
|
||||
[TOGGLE_ALBUM_MONITORED]: (getState, payload, dispatch) => {
|
||||
[TOGGLE_ALBUM_MONITORED]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
artistId: id,
|
||||
seasonNumber,
|
||||
|
|
|
@ -112,7 +112,7 @@ export const actionHandlers = handleThunks({
|
|||
});
|
||||
|
||||
promise.done(() => {
|
||||
// SignaR will take care of removing the serires from the collection
|
||||
// SignalR will take care of removing the artist from the collection
|
||||
|
||||
dispatch(set({
|
||||
section,
|
||||
|
|
|
@ -28,6 +28,7 @@ export const defaultState = {
|
|||
detailedProgressBar: false,
|
||||
size: 'large',
|
||||
showTitle: false,
|
||||
showMonitored: true,
|
||||
showQualityProfile: true
|
||||
},
|
||||
|
||||
|
@ -35,12 +36,14 @@ export const defaultState = {
|
|||
detailedProgressBar: false,
|
||||
size: 'large',
|
||||
showTitle: false,
|
||||
showMonitored: true,
|
||||
showQualityProfile: true
|
||||
},
|
||||
|
||||
overviewOptions: {
|
||||
detailedProgressBar: false,
|
||||
size: 'medium',
|
||||
showMonitored: true,
|
||||
showNetwork: true,
|
||||
showQualityProfile: true,
|
||||
showPreviousAiring: false,
|
||||
|
|
|
@ -2,6 +2,7 @@ import _ from 'lodash';
|
|||
import $ from 'jquery';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import getNewArtist from 'Utilities/Artist/getNewArtist';
|
||||
|
@ -15,14 +16,14 @@ import { fetchRootFolders } from './rootFolderActions';
|
|||
|
||||
export const section = 'importArtist';
|
||||
let concurrentLookups = 0;
|
||||
let abortCurrentLookup = null;
|
||||
const queue = [];
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isLookingUpArtist: false,
|
||||
isImporting: false,
|
||||
isImported: false,
|
||||
importError: null,
|
||||
|
@ -34,9 +35,10 @@ export const defaultState = {
|
|||
|
||||
export const QUEUE_LOOKUP_ARTIST = 'importArtist/queueLookupArtist';
|
||||
export const START_LOOKUP_ARTIST = 'importArtist/startLookupArtist';
|
||||
export const CLEAR_IMPORT_ARTIST = 'importArtist/importArtist';
|
||||
export const SET_IMPORT_ARTIST_VALUE = 'importArtist/clearImportArtist';
|
||||
export const IMPORT_ARTIST = 'importArtist/setImportArtistValue';
|
||||
export const CANCEL_LOOKUP_ARTIST = 'importArtist/cancelLookupArtist';
|
||||
export const CLEAR_IMPORT_ARTIST = 'importArtist/clearImportArtist';
|
||||
export const SET_IMPORT_ARTIST_VALUE = 'importArtist/setImportArtistValue';
|
||||
export const IMPORT_ARTIST = 'importArtist/importArtist';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
@ -45,10 +47,10 @@ export const queueLookupArtist = createThunk(QUEUE_LOOKUP_ARTIST);
|
|||
export const startLookupArtist = createThunk(START_LOOKUP_ARTIST);
|
||||
export const importArtist = createThunk(IMPORT_ARTIST);
|
||||
export const clearImportArtist = createAction(CLEAR_IMPORT_ARTIST);
|
||||
export const cancelLookupArtist = createAction(CANCEL_LOOKUP_ARTIST);
|
||||
|
||||
export const setImportArtistValue = createAction(SET_IMPORT_ARTIST_VALUE, (payload) => {
|
||||
return {
|
||||
|
||||
section,
|
||||
...payload
|
||||
};
|
||||
|
@ -63,7 +65,8 @@ export const actionHandlers = handleThunks({
|
|||
const {
|
||||
name,
|
||||
path,
|
||||
term
|
||||
term,
|
||||
topOfQueue = false
|
||||
} = payload;
|
||||
|
||||
const state = getState().importArtist;
|
||||
|
@ -84,8 +87,20 @@ export const actionHandlers = handleThunks({
|
|||
items: []
|
||||
}));
|
||||
|
||||
const itemIndex = queue.indexOf(item.id);
|
||||
|
||||
if (itemIndex >= 0) {
|
||||
queue.splice(itemIndex, 1);
|
||||
}
|
||||
|
||||
if (topOfQueue) {
|
||||
queue.unshift(item.id);
|
||||
} else {
|
||||
queue.push(item.id);
|
||||
}
|
||||
|
||||
if (term && term.length > 2) {
|
||||
dispatch(startLookupArtist());
|
||||
dispatch(startLookupArtist({ start: true }));
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -95,13 +110,27 @@ export const actionHandlers = handleThunks({
|
|||
}
|
||||
|
||||
const state = getState().importArtist;
|
||||
const queued = _.find(state.items, { queued: true });
|
||||
|
||||
if (!queued) {
|
||||
const {
|
||||
isLookingUpArtist,
|
||||
items
|
||||
} = state;
|
||||
|
||||
const queueId = queue[0];
|
||||
|
||||
if (payload.start && !isLookingUpArtist) {
|
||||
dispatch(set({ section, isLookingUpArtist: true }));
|
||||
} else if (!isLookingUpArtist) {
|
||||
return;
|
||||
} else if (!queueId) {
|
||||
dispatch(set({ section, isLookingUpArtist: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
concurrentLookups++;
|
||||
queue.splice(0, 1);
|
||||
|
||||
const queued = items.find((i) => i.id === queueId);
|
||||
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
|
@ -109,14 +138,16 @@ export const actionHandlers = handleThunks({
|
|||
isFetching: true
|
||||
}));
|
||||
|
||||
const promise = $.ajax({
|
||||
const { request, abortRequest } = createAjaxRequest({
|
||||
url: '/artist/lookup',
|
||||
data: {
|
||||
term: queued.term
|
||||
}
|
||||
});
|
||||
|
||||
promise.done((data) => {
|
||||
abortCurrentLookup = abortRequest;
|
||||
|
||||
request.done((data) => {
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
|
@ -125,23 +156,26 @@ export const actionHandlers = handleThunks({
|
|||
error: null,
|
||||
items: data,
|
||||
queued: false,
|
||||
selectedArtist: queued.selectedArtist || data[0]
|
||||
selectedArtist: queued.selectedArtist || data[0],
|
||||
updateOnly: true
|
||||
}));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
request.fail((xhr) => {
|
||||
dispatch(updateItem({
|
||||
section,
|
||||
id: queued.id,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr,
|
||||
queued: false
|
||||
queued: false,
|
||||
updateOnly: true
|
||||
}));
|
||||
});
|
||||
|
||||
promise.always(() => {
|
||||
request.always(() => {
|
||||
concurrentLookups--;
|
||||
|
||||
dispatch(startLookupArtist());
|
||||
});
|
||||
},
|
||||
|
@ -159,7 +193,7 @@ export const actionHandlers = handleThunks({
|
|||
|
||||
// Make sure we have a selected artist and
|
||||
// the same artist hasn't been added yet.
|
||||
if (selectedArtist && !_.some(acc, { tvdbId: selectedArtist.tvdbId })) {
|
||||
if (selectedArtist && !_.some(acc, { foreignArtistId: selectedArtist.foreignArtistId })) {
|
||||
const newArtist = getNewArtist(_.cloneDeep(selectedArtist), item);
|
||||
newArtist.path = item.path;
|
||||
|
||||
|
@ -216,7 +250,19 @@ export const actionHandlers = handleThunks({
|
|||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CANCEL_LOOKUP_ARTIST]: function(state) {
|
||||
return Object.assign({}, state, { isLookingUpArtist: false });
|
||||
},
|
||||
|
||||
[CLEAR_IMPORT_ARTIST]: function(state) {
|
||||
if (abortCurrentLookup) {
|
||||
abortCurrentLookup();
|
||||
|
||||
abortCurrentLookup = null;
|
||||
}
|
||||
|
||||
queue.splice(0, queue.length);
|
||||
|
||||
return Object.assign({}, state, defaultState);
|
||||
},
|
||||
|
||||
|
|
|
@ -35,24 +35,22 @@ export const addTag = createThunk(ADD_TAG);
|
|||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_TAGS]: createFetchHandler('tags', '/tag'),
|
||||
[FETCH_TAGS]: createFetchHandler(section, '/tag'),
|
||||
|
||||
[ADD_TAG]: function(payload) {
|
||||
return (dispatch, getState) => {
|
||||
const promise = $.ajax({
|
||||
url: '/tag',
|
||||
method: 'POST',
|
||||
data: JSON.stringify(payload.tag)
|
||||
});
|
||||
[ADD_TAG]: function(getState, payload, dispatch) {
|
||||
const promise = $.ajax({
|
||||
url: '/tag',
|
||||
method: 'POST',
|
||||
data: JSON.stringify(payload.tag)
|
||||
});
|
||||
|
||||
promise.done((data) => {
|
||||
const tags = getState().tags.items.slice();
|
||||
tags.push(data);
|
||||
promise.done((data) => {
|
||||
const tags = getState().tags.items.slice();
|
||||
tags.push(data);
|
||||
|
||||
dispatch(update({ section: 'tags', data: tags }));
|
||||
payload.onTagCreated(data);
|
||||
});
|
||||
};
|
||||
dispatch(update({ section, data: tags }));
|
||||
payload.onTagCreated(data);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
const thunks = {};
|
||||
|
||||
export function createThunk(type) {
|
||||
function identity(payload) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function createThunk(type, identityFunction = identity) {
|
||||
return function(payload = {}) {
|
||||
return function(dispatch, getState) {
|
||||
const thunk = thunks[type];
|
||||
|
||||
if (thunk) {
|
||||
return thunk(getState, payload, dispatch);
|
||||
return thunk(getState, identityFunction(payload), dispatch);
|
||||
}
|
||||
|
||||
throw Error(`Thunk handler has not been registered for ${type}`);
|
||||
|
@ -21,4 +25,3 @@ export function handleThunks(handlers) {
|
|||
thunks[type] = handlers[type];
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ module.exports = {
|
|||
sonarrBlue: '#00A65B',
|
||||
helpTextColor: '#909293',
|
||||
gray: '#adadad',
|
||||
disabledInputColor: '#808080',
|
||||
|
||||
// Theme Colors
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue