mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-12 08:07:10 -07:00
New: Write metadata to tags, with UI for previewing changes (#633)
This commit is contained in:
parent
6548f4b1b7
commit
072f772dc8
82 changed files with 2938 additions and 358 deletions
|
@ -7,6 +7,8 @@ import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemTitle from 'Components/DescriptionList/DescriptionListItemTitle';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import styles from './HistoryDetails.css';
|
||||
|
||||
function getDetailedList(statusMessages) {
|
||||
|
@ -36,6 +38,19 @@ function getDetailedList(statusMessages) {
|
|||
);
|
||||
}
|
||||
|
||||
function formatMissing(value) {
|
||||
if (value === undefined || value === 0 || value === '0') {
|
||||
return (<Icon name={icons.BAN} size={12} />);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function formatChange(oldValue, newValue) {
|
||||
return (
|
||||
<div> {formatMissing(oldValue)} <Icon name={icons.ARROW_RIGHT_NO_CIRCLE} size={12} /> {formatMissing(newValue)} </div>
|
||||
);
|
||||
}
|
||||
|
||||
function HistoryDetails(props) {
|
||||
const {
|
||||
eventType,
|
||||
|
@ -259,6 +274,37 @@ function HistoryDetails(props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (eventType === 'trackFileRetagged') {
|
||||
const {
|
||||
diff,
|
||||
tagsScrubbed
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
title="Path"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
{
|
||||
JSON.parse(diff).map(({ field, oldValue, newValue }) => {
|
||||
return (
|
||||
<DescriptionListItem
|
||||
key={field}
|
||||
title={field}
|
||||
data={formatChange(oldValue, newValue)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
<DescriptionListItem
|
||||
title="Existing tags scrubbed"
|
||||
data={tagsScrubbed === 'True' ? <Icon name={icons.CHECK} /> : <Icon name={icons.REMOVE} />}
|
||||
/>
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'albumImportIncomplete') {
|
||||
const {
|
||||
statusMessages
|
||||
|
|
|
@ -23,6 +23,8 @@ function getHeaderTitle(eventType) {
|
|||
return 'Track File Deleted';
|
||||
case 'trackFileRenamed':
|
||||
return 'Track File Renamed';
|
||||
case 'trackFileRetagged':
|
||||
return 'Track File Tags Updated';
|
||||
case 'albumImportIncomplete':
|
||||
return 'Album Import Incomplete';
|
||||
case 'downloadImported':
|
||||
|
|
|
@ -19,6 +19,8 @@ function getIconName(eventType) {
|
|||
return icons.DELETE;
|
||||
case 'trackFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'trackFileRetagged':
|
||||
return icons.RETAG;
|
||||
case 'albumImportIncomplete':
|
||||
return icons.DOWNLOADED;
|
||||
case 'downloadImported':
|
||||
|
@ -53,6 +55,8 @@ function getTooltip(eventType, data) {
|
|||
return 'Track file deleted';
|
||||
case 'trackFileRenamed':
|
||||
return 'Track file renamed';
|
||||
case 'trackFileRetagged':
|
||||
return 'Track file tags updated';
|
||||
case 'albumImportIncomplete':
|
||||
return 'Files downloaded but not all could be imported';
|
||||
case 'downloadImported':
|
||||
|
|
|
@ -16,6 +16,7 @@ import MonitorToggleButton from 'Components/MonitorToggleButton';
|
|||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import AlbumCover from 'Album/AlbumCover';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
@ -82,6 +83,7 @@ class AlbumDetails extends Component {
|
|||
|
||||
this.state = {
|
||||
isOrganizeModalOpen: false,
|
||||
isRetagModalOpen: false,
|
||||
isArtistHistoryModalOpen: false,
|
||||
isInteractiveSearchModalOpen: false,
|
||||
isManageTracksOpen: false,
|
||||
|
@ -103,6 +105,14 @@ class AlbumDetails extends Component {
|
|||
this.setState({ isOrganizeModalOpen: false });
|
||||
}
|
||||
|
||||
onRetagPress = () => {
|
||||
this.setState({ isRetagModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagModalClose = () => {
|
||||
this.setState({ isRetagModalOpen: false });
|
||||
}
|
||||
|
||||
onEditAlbumPress = () => {
|
||||
this.setState({ isEditAlbumModalOpen: true });
|
||||
}
|
||||
|
@ -193,6 +203,7 @@ class AlbumDetails extends Component {
|
|||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
isArtistHistoryModalOpen,
|
||||
isInteractiveSearchModalOpen,
|
||||
isEditAlbumModalOpen,
|
||||
|
@ -235,6 +246,12 @@ class AlbumDetails extends Component {
|
|||
onPress={this.onOrganizePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Preview Retag"
|
||||
iconName={icons.RETAG}
|
||||
onPress={this.onRetagPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manage Tracks"
|
||||
iconName={icons.TRACK_FILE}
|
||||
|
@ -495,6 +512,13 @@ class AlbumDetails extends Component {
|
|||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<RetagPreviewModalConnector
|
||||
isOpen={isRetagModalOpen}
|
||||
artistId={artist.id}
|
||||
albumId={id}
|
||||
onModalClose={this.onRetagModalClose}
|
||||
/>
|
||||
|
||||
<TrackFileEditorModal
|
||||
isOpen={isManageTracksOpen}
|
||||
artistId={artist.id}
|
||||
|
|
|
@ -23,6 +23,7 @@ import Popover from 'Components/Tooltip/Popover';
|
|||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import TrackFileEditorModal from 'TrackFile/Editor/TrackFileEditorModal';
|
||||
import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector';
|
||||
import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector';
|
||||
import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfileNameConnector';
|
||||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
|
@ -66,6 +67,7 @@ class ArtistDetails extends Component {
|
|||
|
||||
this.state = {
|
||||
isOrganizeModalOpen: false,
|
||||
isRetagModalOpen: false,
|
||||
isManageTracksOpen: false,
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false,
|
||||
|
@ -89,6 +91,14 @@ class ArtistDetails extends Component {
|
|||
this.setState({ isOrganizeModalOpen: false });
|
||||
}
|
||||
|
||||
onRetagPress = () => {
|
||||
this.setState({ isRetagModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagModalClose = () => {
|
||||
this.setState({ isRetagModalOpen: false });
|
||||
}
|
||||
|
||||
onManageTracksPress = () => {
|
||||
this.setState({ isManageTracksOpen: true });
|
||||
}
|
||||
|
@ -207,6 +217,7 @@ class ArtistDetails extends Component {
|
|||
|
||||
const {
|
||||
isOrganizeModalOpen,
|
||||
isRetagModalOpen,
|
||||
isManageTracksOpen,
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen,
|
||||
|
@ -276,6 +287,12 @@ class ArtistDetails extends Component {
|
|||
onPress={this.onOrganizePress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Preview Retag"
|
||||
iconName={icons.RETAG}
|
||||
onPress={this.onRetagPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Manage Tracks"
|
||||
iconName={icons.TRACK_FILE}
|
||||
|
@ -600,6 +617,12 @@ class ArtistDetails extends Component {
|
|||
onModalClose={this.onOrganizeModalClose}
|
||||
/>
|
||||
|
||||
<RetagPreviewModalConnector
|
||||
isOpen={isRetagModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onRetagModalClose}
|
||||
/>
|
||||
|
||||
<TrackFileEditorModal
|
||||
isOpen={isManageTracksOpen}
|
||||
artistId={id}
|
||||
|
|
|
@ -14,6 +14,7 @@ import Table from 'Components/Table/Table';
|
|||
import TableBody from 'Components/Table/TableBody';
|
||||
import NoArtist from 'Artist/NoArtist';
|
||||
import OrganizeArtistModal from './Organize/OrganizeArtistModal';
|
||||
import RetagArtistModal from './AudioTags/RetagArtistModal';
|
||||
import ArtistEditorRowConnector from './ArtistEditorRowConnector';
|
||||
import ArtistEditorFooter from './ArtistEditorFooter';
|
||||
import ArtistEditorFilterModalConnector from './ArtistEditorFilterModalConnector';
|
||||
|
@ -84,6 +85,7 @@ class ArtistEditor extends Component {
|
|||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isOrganizingArtistModalOpen: false,
|
||||
isRetaggingArtistModalOpen: false,
|
||||
columns: getColumns(props.showLanguageProfile, props.showMetadataProfile)
|
||||
};
|
||||
}
|
||||
|
@ -142,6 +144,18 @@ class ArtistEditor extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
onRetagArtistPress = () => {
|
||||
this.setState({ isRetaggingArtistModalOpen: true });
|
||||
}
|
||||
|
||||
onRetagArtistModalClose = (organized) => {
|
||||
this.setState({ isRetaggingArtistModalOpen: false });
|
||||
|
||||
if (organized === true) {
|
||||
this.onSelectAllChange({ value: false });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
|
@ -162,6 +176,7 @@ class ArtistEditor extends Component {
|
|||
isDeleting,
|
||||
deleteError,
|
||||
isOrganizingArtist,
|
||||
isRetaggingArtist,
|
||||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
onSortPress,
|
||||
|
@ -250,10 +265,12 @@ class ArtistEditor extends Component {
|
|||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
isOrganizingArtist={isOrganizingArtist}
|
||||
isRetaggingArtist={isRetaggingArtist}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
showMetadataProfile={showMetadataProfile}
|
||||
onSaveSelected={this.onSaveSelected}
|
||||
onOrganizeArtistPress={this.onOrganizeArtistPress}
|
||||
onRetagArtistPress={this.onRetagArtistPress}
|
||||
/>
|
||||
|
||||
<OrganizeArtistModal
|
||||
|
@ -261,6 +278,13 @@ class ArtistEditor extends Component {
|
|||
artistIds={selectedArtistIds}
|
||||
onModalClose={this.onOrganizeArtistModalClose}
|
||||
/>
|
||||
|
||||
<RetagArtistModal
|
||||
isOpen={this.state.isRetaggingArtistModalOpen}
|
||||
artistIds={selectedArtistIds}
|
||||
onModalClose={this.onRetagArtistModalClose}
|
||||
/>
|
||||
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
@ -282,6 +306,7 @@ ArtistEditor.propTypes = {
|
|||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingArtist: PropTypes.bool.isRequired,
|
||||
isRetaggingArtist: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onSortPress: PropTypes.func.isRequired,
|
||||
|
|
|
@ -16,9 +16,11 @@ function createMapStateToProps() {
|
|||
(state) => state.settings.metadataProfiles,
|
||||
createClientSideCollectionSelector('artist', 'artistEditor'),
|
||||
createCommandExecutingSelector(commandNames.RENAME_ARTIST),
|
||||
(languageProfiles, metadataProfiles, artist, isOrganizingArtist) => {
|
||||
createCommandExecutingSelector(commandNames.RETAG_ARTIST),
|
||||
(languageProfiles, metadataProfiles, artist, isOrganizingArtist, isRetaggingArtist) => {
|
||||
return {
|
||||
isOrganizingArtist,
|
||||
isRetaggingArtist,
|
||||
showLanguageProfile: languageProfiles.items.length > 1,
|
||||
showMetadataProfile: metadataProfiles.items.length > 1,
|
||||
...artist
|
||||
|
|
|
@ -145,9 +145,11 @@ class ArtistEditorFooter extends Component {
|
|||
isSaving,
|
||||
isDeleting,
|
||||
isOrganizingArtist,
|
||||
isRetaggingArtist,
|
||||
showLanguageProfile,
|
||||
showMetadataProfile,
|
||||
onOrganizeArtistPress
|
||||
onOrganizeArtistPress,
|
||||
onRetagArtistPress
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
|
@ -288,19 +290,29 @@ class ArtistEditorFooter extends Component {
|
|||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
isSpinning={isOrganizingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
|
||||
onPress={onOrganizeArtistPress}
|
||||
>
|
||||
Rename Files
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.organizeSelectedButton}
|
||||
kind={kinds.WARNING}
|
||||
isSpinning={isRetaggingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
|
||||
onPress={onRetagArtistPress}
|
||||
>
|
||||
Write Metadata Tags
|
||||
</SpinnerButton>
|
||||
|
||||
<SpinnerButton
|
||||
className={styles.tagsButton}
|
||||
isSpinning={isSaving && savingTags}
|
||||
isDisabled={!selectedCount || isOrganizingArtist}
|
||||
isDisabled={!selectedCount || isOrganizingArtist || isRetaggingArtist}
|
||||
onPress={this.onTagsPress}
|
||||
>
|
||||
Set Tags
|
||||
Set Lidarr Tags
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
|
||||
|
@ -350,10 +362,12 @@ ArtistEditorFooter.propTypes = {
|
|||
isDeleting: PropTypes.bool.isRequired,
|
||||
deleteError: PropTypes.object,
|
||||
isOrganizingArtist: PropTypes.bool.isRequired,
|
||||
isRetaggingArtist: PropTypes.bool.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
showMetadataProfile: PropTypes.bool.isRequired,
|
||||
onSaveSelected: PropTypes.func.isRequired,
|
||||
onOrganizeArtistPress: PropTypes.func.isRequired
|
||||
onOrganizeArtistPress: PropTypes.func.isRequired,
|
||||
onRetagArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistEditorFooter;
|
||||
|
|
31
frontend/src/Artist/Editor/AudioTags/RetagArtistModal.js
Normal file
31
frontend/src/Artist/Editor/AudioTags/RetagArtistModal.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import RetagArtistModalContentConnector from './RetagArtistModalContentConnector';
|
||||
|
||||
function RetagArtistModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<RetagArtistModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
RetagArtistModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RetagArtistModal;
|
|
@ -0,0 +1,8 @@
|
|||
.retagIcon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { icons, kinds } from 'Helpers/Props';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import Icon from 'Components/Icon';
|
||||
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 './RetagArtistModalContent.css';
|
||||
|
||||
function RetagArtistModalContent(props) {
|
||||
const {
|
||||
artistNames,
|
||||
onModalClose,
|
||||
onRetagArtistPress
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Retag Selected Artist
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert>
|
||||
Tip: To preview the tags that will be written... select "Cancel" then click any artist name and use the
|
||||
<Icon
|
||||
className={styles.retagIcon}
|
||||
name={icons.RETAG}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.message}>
|
||||
Are you sure you want to re-tag all files in the {artistNames.length} selected artist?
|
||||
</div>
|
||||
<ul>
|
||||
{
|
||||
artistNames.map((artistName) => {
|
||||
return (
|
||||
<li key={artistName}>
|
||||
{artistName}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={onRetagArtistPress}
|
||||
>
|
||||
Retag
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
RetagArtistModalContent.propTypes = {
|
||||
artistNames: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
onRetagArtistPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RetagArtistModalContent;
|
|
@ -0,0 +1,67 @@
|
|||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createAllArtistSelector from 'Store/Selectors/createAllArtistSelector';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import RetagArtistModalContent from './RetagArtistModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { artistIds }) => artistIds,
|
||||
createAllArtistSelector(),
|
||||
(artistIds, allArtists) => {
|
||||
const artist = _.intersectionWith(allArtists, artistIds, (s, id) => {
|
||||
return s.id === id;
|
||||
});
|
||||
|
||||
const sortedArtist = _.orderBy(artist, 'sortName');
|
||||
const artistNames = _.map(sortedArtist, 'artistName');
|
||||
|
||||
return {
|
||||
artistNames
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class RetagArtistModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRetagArtistPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RETAG_ARTIST,
|
||||
artistIds: this.props.artistIds
|
||||
});
|
||||
|
||||
this.props.onModalClose(true);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render(props) {
|
||||
return (
|
||||
<RetagArtistModalContent
|
||||
{...this.props}
|
||||
onRetagArtistPress={this.onRetagArtistPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RetagArtistModalContentConnector.propTypes = {
|
||||
artistIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
onModalClose: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(RetagArtistModalContentConnector);
|
|
@ -14,6 +14,8 @@ export const MOVE_ARTIST = 'MoveArtist';
|
|||
export const REFRESH_ARTIST = 'RefreshArtist';
|
||||
export const RENAME_FILES = 'RenameFiles';
|
||||
export const RENAME_ARTIST = 'RenameArtist';
|
||||
export const RETAG_FILES = 'RetagFiles';
|
||||
export const RETAG_ARTIST = 'RetagArtist';
|
||||
export const RESET_API_KEY = 'ResetApiKey';
|
||||
export const RSS_SYNC = 'RssSync';
|
||||
export const SEASON_SEARCH = 'AlbumSearch';
|
||||
|
|
|
@ -25,7 +25,9 @@ import {
|
|||
faArrowCircleLeft as fasArrowCircleLeft,
|
||||
faArrowCircleRight as fasArrowCircleRight,
|
||||
faArrowCircleUp as fasArrowCircleUp,
|
||||
faLongArrowAltRight as fasLongArrowAltRight,
|
||||
faBackward as fasBackward,
|
||||
faBan as fasBan,
|
||||
faBars as fasBars,
|
||||
faBolt as fasBolt,
|
||||
faBookmark as fasBookmark,
|
||||
|
@ -47,6 +49,7 @@ import {
|
|||
faCopy as fasCopy,
|
||||
faDesktop as fasDesktop,
|
||||
faDownload as fasDownload,
|
||||
faEdit as fasEdit,
|
||||
faEllipsisH as fasEllipsisH,
|
||||
faExclamationCircle as fasExclamationCircle,
|
||||
faExclamationTriangle as fasExclamationTriangle,
|
||||
|
@ -111,8 +114,10 @@ export const ALTERNATE_TITLES = farClone;
|
|||
export const ADVANCED_SETTINGS = fasCog;
|
||||
export const ARROW_LEFT = fasArrowCircleLeft;
|
||||
export const ARROW_RIGHT = fasArrowCircleRight;
|
||||
export const ARROW_RIGHT_NO_CIRCLE = fasLongArrowAltRight;
|
||||
export const ARROW_UP = fasArrowCircleUp;
|
||||
export const BACKUP = farFileArchive;
|
||||
export const BAN = fasBan;
|
||||
export const BUG = fasBug;
|
||||
export const CALENDAR = fasCalendarAlt;
|
||||
export const CALENDAR_O = farCalendar;
|
||||
|
@ -176,9 +181,10 @@ export const QUEUED = fasCloud;
|
|||
export const QUICK = fasRocket;
|
||||
export const REFRESH = fasSync;
|
||||
export const REMOVE = fasTimes;
|
||||
export const REORDER = fasBars;
|
||||
export const RESTART = fasRedoAlt;
|
||||
export const RESTORE = fasHistory;
|
||||
export const REORDER = fasBars;
|
||||
export const RETAG = fasEdit;
|
||||
export const RSS = fasRss;
|
||||
export const SAVE = fasSave;
|
||||
export const SCHEDULED = farClock;
|
||||
|
|
|
@ -74,7 +74,6 @@ class OrganizePreviewModalContent extends Component {
|
|||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
renameTracks,
|
||||
trackFormat,
|
||||
path,
|
||||
onModalClose
|
||||
|
@ -107,13 +106,7 @@ class OrganizePreviewModalContent extends Component {
|
|||
|
||||
{
|
||||
!isFetching && isPopulated && !items.length &&
|
||||
<div>
|
||||
{
|
||||
renameTracks ?
|
||||
<div>Success! My work is done, no files to rename.</div> :
|
||||
<div>Renaming is disabled, nothing to rename</div>
|
||||
}
|
||||
</div>
|
||||
<div>Success! My work is done, no files to rename.</div>
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -191,7 +184,6 @@ OrganizePreviewModalContent.propTypes = {
|
|||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
renameTracks: PropTypes.bool,
|
||||
trackFormat: PropTypes.string,
|
||||
onOrganizePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
|
|
|
@ -19,7 +19,6 @@ function createMapStateToProps() {
|
|||
props.isFetching = organizePreview.isFetching || naming.isFetching;
|
||||
props.isPopulated = organizePreview.isPopulated && naming.isPopulated;
|
||||
props.error = organizePreview.error || naming.error;
|
||||
props.renameTracks = naming.item.renameTracks;
|
||||
props.trackFormat = naming.item.standardTrackFormat;
|
||||
props.path = artist.path;
|
||||
|
||||
|
|
34
frontend/src/Retag/RetagPreviewModal.js
Normal file
34
frontend/src/Retag/RetagPreviewModal.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import RetagPreviewModalContentConnector from './RetagPreviewModalContentConnector';
|
||||
|
||||
function RetagPreviewModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
isOpen &&
|
||||
<RetagPreviewModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
RetagPreviewModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RetagPreviewModal;
|
39
frontend/src/Retag/RetagPreviewModalConnector.js
Normal file
39
frontend/src/Retag/RetagPreviewModalConnector.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearRetagPreview } from 'Store/Actions/retagPreviewActions';
|
||||
import RetagPreviewModal from './RetagPreviewModal';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
clearRetagPreview
|
||||
};
|
||||
|
||||
class RetagPreviewModalConnector extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onModalClose = () => {
|
||||
this.props.clearRetagPreview();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RetagPreviewModal
|
||||
{...this.props}
|
||||
onModalClose={this.onModalClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RetagPreviewModalConnector.propTypes = {
|
||||
clearRetagPreview: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(undefined, mapDispatchToProps)(RetagPreviewModalConnector);
|
24
frontend/src/Retag/RetagPreviewModalContent.css
Normal file
24
frontend/src/Retag/RetagPreviewModalContent.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
.path {
|
||||
margin-left: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.trackFormat {
|
||||
margin-left: 5px;
|
||||
font-family: $monoSpaceFontFamily;
|
||||
}
|
||||
|
||||
.previews {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.selectAllInputContainer {
|
||||
margin-right: auto;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.selectAllInput {
|
||||
composes: input from '~Components/Form/CheckInput.css';
|
||||
|
||||
margin: 0;
|
||||
}
|
186
frontend/src/Retag/RetagPreviewModalContent.js
Normal file
186
frontend/src/Retag/RetagPreviewModalContent.js
Normal file
|
@ -0,0 +1,186 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import Alert from 'Components/Alert';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
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 CheckInput from 'Components/Form/CheckInput';
|
||||
import RetagPreviewRow from './RetagPreviewRow';
|
||||
import styles from './RetagPreviewModalContent.css';
|
||||
|
||||
function getValue(allSelected, allUnselected) {
|
||||
if (allSelected) {
|
||||
return true;
|
||||
} else if (allUnselected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
class RetagPreviewModalContent extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
getSelectedIds = () => {
|
||||
return getSelectedIds(this.state.selectedState);
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectAllChange = ({ value }) => {
|
||||
this.setState(selectAll(this.state.selectedState, value));
|
||||
}
|
||||
|
||||
onSelectedChange = ({ id, value, shiftKey = false }) => {
|
||||
this.setState((state) => {
|
||||
return toggleSelected(state, this.props.items, id, value, shiftKey);
|
||||
});
|
||||
}
|
||||
|
||||
onRetagPress = () => {
|
||||
this.props.onRetagPress(this.getSelectedIds());
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
path,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState
|
||||
} = this.state;
|
||||
|
||||
const selectAllValue = getValue(allSelected, allUnselected);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Write Metadata Tags
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && error &&
|
||||
<div>Error loading previews</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && ((isPopulated && !items.length)) &&
|
||||
<div>Success! My work is done, no files to retag.</div>
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && isPopulated && !!items.length &&
|
||||
<div>
|
||||
<Alert>
|
||||
<div>
|
||||
All paths are relative to:
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
MusicBrainz identifiers will also be added to the files; these are not shown below.
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<div className={styles.previews}>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<RetagPreviewRow
|
||||
key={item.trackFileId}
|
||||
id={item.trackFileId}
|
||||
path={item.relativePath}
|
||||
changes={item.changes}
|
||||
isSelected={selectedState[item.trackFileId]}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{
|
||||
isPopulated && !!items.length &&
|
||||
<CheckInput
|
||||
className={styles.selectAllInput}
|
||||
containerClassName={styles.selectAllInputContainer}
|
||||
name="selectAll"
|
||||
value={selectAllValue}
|
||||
onChange={this.onSelectAllChange}
|
||||
/>
|
||||
}
|
||||
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
kind={kinds.PRIMARY}
|
||||
onPress={this.onRetagPress}
|
||||
>
|
||||
Retag
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RetagPreviewModalContent.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
onRetagPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RetagPreviewModalContent;
|
86
frontend/src/Retag/RetagPreviewModalContentConnector.js
Normal file
86
frontend/src/Retag/RetagPreviewModalContentConnector.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
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 { fetchRetagPreview } from 'Store/Actions/retagPreviewActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import RetagPreviewModalContent from './RetagPreviewModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.retagPreview,
|
||||
createArtistSelector(),
|
||||
(retagPreview, artist) => {
|
||||
const props = { ...retagPreview };
|
||||
props.isFetching = retagPreview.isFetching;
|
||||
props.isPopulated = retagPreview.isPopulated;
|
||||
props.error = retagPreview.error;
|
||||
props.path = artist.path;
|
||||
|
||||
return props;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchRetagPreview,
|
||||
executeCommand
|
||||
};
|
||||
|
||||
class RetagPreviewModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
artistId,
|
||||
albumId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchRetagPreview({
|
||||
artistId,
|
||||
albumId
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRetagPress = (files) => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.RETAG_FILES,
|
||||
artistId: this.props.artistId,
|
||||
files
|
||||
});
|
||||
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<RetagPreviewModalContent
|
||||
{...this.props}
|
||||
onRetagPress={this.onRetagPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RetagPreviewModalContentConnector.propTypes = {
|
||||
artistId: PropTypes.number.isRequired,
|
||||
albumId: PropTypes.number,
|
||||
retagTracks: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
fetchRetagPreview: PropTypes.func.isRequired,
|
||||
executeCommand: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(RetagPreviewModalContentConnector);
|
26
frontend/src/Retag/RetagPreviewRow.css
Normal file
26
frontend/src/Retag/RetagPreviewRow.css
Normal file
|
@ -0,0 +1,26 @@
|
|||
.row {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.selectedContainer {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.path {
|
||||
margin-left: 10px;
|
||||
font-weight: bold;
|
||||
}
|
101
frontend/src/Retag/RetagPreviewRow.js
Normal file
101
frontend/src/Retag/RetagPreviewRow.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import CheckInput from 'Components/Form/CheckInput';
|
||||
import styles from './RetagPreviewRow.css';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
|
||||
function formatMissing(value) {
|
||||
if (value === undefined || value === 0 || value === '0') {
|
||||
return (<Icon name={icons.BAN} size={12} />);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function formatChange(oldValue, newValue) {
|
||||
return (
|
||||
<div> {formatMissing(oldValue)} <Icon name={icons.ARROW_RIGHT_NO_CIRCLE} size={12} /> {formatMissing(newValue)} </div>
|
||||
);
|
||||
}
|
||||
|
||||
class RetagPreviewRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value: true });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onSelectedChange = ({ value, shiftKey }) => {
|
||||
const {
|
||||
id,
|
||||
onSelectedChange
|
||||
} = this.props;
|
||||
|
||||
onSelectedChange({ id, value, shiftKey });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
id,
|
||||
path,
|
||||
changes,
|
||||
isSelected
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.row}>
|
||||
<CheckInput
|
||||
containerClassName={styles.selectedContainer}
|
||||
name={id.toString()}
|
||||
value={isSelected}
|
||||
onChange={this.onSelectedChange}
|
||||
/>
|
||||
|
||||
<div className={styles.column}>
|
||||
<span className={styles.path}>
|
||||
{path}
|
||||
</span>
|
||||
|
||||
<DescriptionList>
|
||||
{
|
||||
changes.map(({ field, oldValue, newValue }) => {
|
||||
return (
|
||||
<DescriptionListItem
|
||||
key={field}
|
||||
title={field}
|
||||
data={formatChange(oldValue, newValue)}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</DescriptionList>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RetagPreviewRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
path: PropTypes.string.isRequired,
|
||||
changes: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isSelected: PropTypes.bool,
|
||||
onSelectedChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default RetagPreviewRow;
|
|
@ -8,6 +8,13 @@ import FormGroup from 'Components/Form/FormGroup';
|
|||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
|
||||
const writeAudioTagOptions = [
|
||||
{ key: 'sync', value: 'All files; keep in sync with MusicBrainz' },
|
||||
{ key: 'allFiles', value: 'All files; initial import only' },
|
||||
{ key: 'newFiles', value: 'For new downloads only' },
|
||||
{ key: 'no', value: 'Never' }
|
||||
];
|
||||
|
||||
function MetadataProvider(props) {
|
||||
const {
|
||||
advancedSettings,
|
||||
|
@ -54,6 +61,35 @@ function MetadataProvider(props) {
|
|||
</FormGroup>
|
||||
</FieldSet>
|
||||
}
|
||||
|
||||
<FieldSet legend="Write Metadata to Audio Files">
|
||||
<FormGroup>
|
||||
<FormLabel>Tag Audio Files with Metadata</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="writeAudioTags"
|
||||
helpTextWarning="Selecting 'All files' will alter existing files when they are imported."
|
||||
helpLink="https://github.com/Lidarr/Lidarr/wiki/Write-Tags"
|
||||
values={writeAudioTagOptions}
|
||||
onChange={onInputChange}
|
||||
{...settings.writeAudioTags}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Scrub Existing Tags</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="scrubAudioTags"
|
||||
helpText="Remove existing tags from files, leaving only those added by Lidarr."
|
||||
onChange={onInputChange}
|
||||
{...settings.scrubAudioTags}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
</FieldSet>
|
||||
</Form>
|
||||
}
|
||||
</div>
|
||||
|
|
|
@ -55,11 +55,11 @@ class MetadataSettings extends Component {
|
|||
/>
|
||||
|
||||
<PageContentBodyConnector>
|
||||
<MetadatasConnector />
|
||||
<MetadataProviderConnector
|
||||
onChildMounted={this.onChildMounted}
|
||||
onChildStateChange={this.onChildStateChange}
|
||||
/>
|
||||
<MetadatasConnector />
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
|
|
|
@ -173,6 +173,17 @@ export const defaultState = {
|
|||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'retagged',
|
||||
label: 'Retagged',
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '9',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import * as importArtist from './importArtistActions';
|
|||
import * as interactiveImportActions from './interactiveImportActions';
|
||||
import * as oAuth from './oAuthActions';
|
||||
import * as organizePreview from './organizePreviewActions';
|
||||
import * as retagPreview from './retagPreviewActions';
|
||||
import * as paths from './pathActions';
|
||||
import * as queue from './queueActions';
|
||||
import * as releases from './releaseActions';
|
||||
|
@ -46,6 +47,7 @@ export default [
|
|||
interactiveImportActions,
|
||||
oAuth,
|
||||
organizePreview,
|
||||
retagPreview,
|
||||
paths,
|
||||
queue,
|
||||
releases,
|
||||
|
|
51
frontend/src/Store/Actions/retagPreviewActions.js
Normal file
51
frontend/src/Store/Actions/retagPreviewActions.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { createAction } from 'redux-actions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'retagPreview';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_RETAG_PREVIEW = 'retagPreview/fetchRetagPreview';
|
||||
export const CLEAR_RETAG_PREVIEW = 'retagPreview/clearRetagPreview';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchRetagPreview = createThunk(FETCH_RETAG_PREVIEW);
|
||||
export const clearRetagPreview = createAction(CLEAR_RETAG_PREVIEW);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_RETAG_PREVIEW]: createFetchHandler('retagPreview', '/retag')
|
||||
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_RETAG_PREVIEW]: (state) => {
|
||||
return Object.assign({}, state, defaultState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
Loading…
Add table
Add a link
Reference in a new issue