From 8a20c0fa83d2944195ab9026eb33fe647fd4b3a1 Mon Sep 17 00:00:00 2001 From: ta264 Date: Mon, 16 Dec 2019 21:21:32 +0000 Subject: [PATCH] New: Add/remove individual albums --- .../AddNewArtist/AddNewArtistModalContent.js | 241 ----------------- .../ArtistMetadataProfilePopoverContent.js | 11 + .../Import/ImportArtistConnector.js | 12 +- frontend/src/Album/Delete/DeleteAlbumModal.js | 33 +++ .../Album/Delete/DeleteAlbumModalContent.css | 12 + .../Album/Delete/DeleteAlbumModalContent.js | 157 +++++++++++ .../DeleteAlbumModalContentConnector.js | 62 +++++ frontend/src/Album/Details/AlbumDetails.js | 28 ++ .../Details/AlbumDetailsPageConnector.js | 13 + frontend/src/App/AppRoutes.js | 6 +- frontend/src/Artist/Details/ArtistDetails.js | 6 +- .../Artist/Details/ArtistDetailsConnector.js | 17 +- .../Artist/Edit/EditArtistModalContent.css | 4 + .../src/Artist/Edit/EditArtistModalContent.js | 23 +- frontend/src/Artist/NoArtist.js | 4 +- .../MetadataProfileSelectInputConnector.js | 18 +- .../Page/Header/ArtistSearchInput.js | 2 +- .../Page/Header/ArtistSearchInputConnector.js | 2 +- .../Components/Page/Sidebar/PageSidebar.js | 2 +- frontend/src/Components/SignalRConnector.js | 12 +- frontend/src/Helpers/Props/index.js | 2 + .../src/Helpers/Props/metadataProfileNames.js | 1 + .../AddNewItem.css} | 0 .../AddNewArtist.js => Search/AddNewItem.js} | 74 ++++-- .../AddNewItemConnector.js} | 54 ++-- frontend/src/Search/Album/AddNewAlbumModal.js | 31 +++ .../Search/Album/AddNewAlbumModalContent.css | 126 +++++++++ .../Search/Album/AddNewAlbumModalContent.js | 157 +++++++++++ .../Album/AddNewAlbumModalContentConnector.js | 135 ++++++++++ .../Search/Album/AddNewAlbumSearchResult.css | 64 +++++ .../Search/Album/AddNewAlbumSearchResult.js | 250 ++++++++++++++++++ .../Album/AddNewAlbumSearchResultConnector.js | 17 ++ .../Artist}/AddNewArtistModal.js | 0 .../Artist}/AddNewArtistModalContent.css | 22 +- .../Search/Artist/AddNewArtistModalContent.js | 146 ++++++++++ .../AddNewArtistModalContentConnector.js | 16 +- .../Artist}/AddNewArtistSearchResult.css | 0 .../Artist}/AddNewArtistSearchResult.js | 1 + .../AddNewArtistSearchResultConnector.js | 0 .../Search/Common/AddArtistOptionsForm.css | 9 + .../src/Search/Common/AddArtistOptionsForm.js | 160 +++++++++++ .../EditImportListExclusionModalContent.js | 6 +- .../Profiles/Metadata/MetadataProfiles.js | 4 +- frontend/src/Store/Actions/albumActions.js | 13 + frontend/src/Store/Actions/index.js | 4 +- .../{addArtistActions.js => searchActions.js} | 86 ++++-- frontend/src/Utilities/Album/getNewAlbum.js | 18 ++ src/Lidarr.Api.V1/Albums/AlbumModule.cs | 52 +++- src/Lidarr.Api.V1/Albums/AlbumResource.cs | 10 +- .../Metadata/MetadataProfileModule.cs | 2 +- src/Lidarr.Api.V1/Search/SearchModule.cs | 71 +++++ src/Lidarr.Api.V1/Search/SearchResource.cs | 14 + src/NzbDrone.Automation.Test/MainPagesTest.cs | 2 +- .../TrackedDownloadServiceFixture.cs | 2 +- .../ImportListSyncServiceFixture.cs | 99 ++++--- .../SkyHook/SkyHookProxyFixture.cs | 1 - .../SkyHook/SkyHookProxySearchFixture.cs | 26 +- .../MusicTests/AddAlbumFixture.cs | 96 +++++++ .../MusicTests/AddArtistFixture.cs | 1 - .../MusicTests/RefreshAlbumServiceFixture.cs | 2 +- .../MusicTests/RefreshArtistServiceFixture.cs | 30 ++- .../Metadata/MetadataProfileServiceFixture.cs | 76 +++++- .../ArtistStats/ArtistStatisticsService.cs | 16 ++ .../ImportListExclusionRepository.cs | 7 + .../Exclusions/ImportListExclusionService.cs | 43 ++- .../ImportLists/ImportListSyncService.cs | 222 +++++++++++----- .../MediaCover/MediaCoverService.cs | 9 + .../MediaFiles/MediaFileDeletionService.cs | 13 + .../MediaFiles/MediaFileRepository.cs | 8 + .../MediaFiles/MediaFileService.cs | 9 +- .../MetadataSource/ISearchForNewEntity.cs | 10 + .../SkyHook/Resource/EntityResource.cs | 10 + .../MetadataSource/SkyHook/SkyHookProxy.cs | 49 +++- .../Music/ArtistNameNormalizer.cs | 28 -- .../Music/Events/AlbumAddedEvent.cs | 4 - .../Music/Events/AlbumDeletedEvent.cs | 4 +- .../Music/Events/AlbumUpdatedEvent.cs | 14 + .../Music/Events/ArtistAddedEvent.cs | 8 +- .../Music/Handlers/AlbumAddedHandler.cs | 22 ++ .../{ => Handlers}/ArtistAddedHandler.cs | 5 +- .../{ => Handlers}/ArtistScannedHandler.cs | 27 +- .../Music/Model/AddAlbumOptions.cs | 22 ++ .../Music/{ => Model}/AddArtistOptions.cs | 5 - src/NzbDrone.Core/Music/{ => Model}/Album.cs | 20 +- src/NzbDrone.Core/Music/{ => Model}/Artist.cs | 26 +- .../Music/{ => Model}/ArtistMetadata.cs | 0 .../Music/{ => Model}/ArtistStatusType.cs | 0 src/NzbDrone.Core/Music/{ => Model}/Entity.cs | 0 src/NzbDrone.Core/Music/{ => Model}/Links.cs | 0 src/NzbDrone.Core/Music/{ => Model}/Medium.cs | 0 src/NzbDrone.Core/Music/{ => Model}/Member.cs | 0 .../Music/{ => Model}/MonitoringOptions.cs | 0 .../Music/{ => Model}/PrimaryAlbumType.cs | 0 .../Music/{ => Model}/Ratings.cs | 0 .../Music/{ => Model}/Release.cs | 0 .../Music/{ => Model}/ReleaseStatus.cs | 0 .../Music/{ => Model}/SecondaryAlbumType.cs | 0 src/NzbDrone.Core/Music/{ => Model}/Track.cs | 0 .../{ => Repositories}/AlbumRepository.cs | 9 +- .../ArtistMetadataRepository.cs | 2 +- .../{ => Repositories}/ArtistRepository.cs | 2 +- .../{ => Repositories}/ReleaseRepository.cs | 0 .../{ => Repositories}/TrackRepository.cs | 12 +- .../Music/Services/AddAlbumService.cs | 113 ++++++++ .../Music/{ => Services}/AddArtistService.cs | 19 +- .../Music/{ => Services}/AlbumAddedService.cs | 23 +- .../{ => Services}/AlbumCutoffService.cs | 5 +- .../{ => Services}/AlbumEditedService.cs | 10 +- .../{ => Services}/AlbumMonitoredService.cs | 8 +- .../Music/{ => Services}/AlbumService.cs | 30 +-- .../{ => Services}/ArtistEditedService.cs | 0 .../{ => Services}/ArtistMetadataService.cs | 6 +- .../Music/{ => Services}/ArtistService.cs | 21 +- .../Music/{ => Services}/MoveArtistService.cs | 1 - .../RefreshAlbumReleaseService.cs | 10 +- .../{ => Services}/RefreshAlbumService.cs | 31 ++- .../{ => Services}/RefreshArtistService.cs | 7 +- .../RefreshEntityServiceBase.cs | 23 +- .../{ => Services}/RefreshTrackService.cs | 2 +- .../Music/{ => Services}/ReleaseService.cs | 0 .../Music/{ => Services}/TrackService.cs | 6 +- .../{ => Utilities}/AddArtistValidator.cs | 0 .../{ => Utilities}/ArtistPathBuilder.cs | 0 .../{ => Utilities}/ShouldRefreshAlbum.cs | 0 .../{ => Utilities}/ShouldRefreshArtist.cs | 2 - .../Profiles/Metadata/MetadataProfile.cs | 1 - .../Metadata/MetadataProfileService.cs | 55 +++- .../MetadataProfileExistsValidator.cs | 1 + 128 files changed, 2789 insertions(+), 736 deletions(-) delete mode 100644 frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.js create mode 100644 frontend/src/AddArtist/ArtistMetadataProfilePopoverContent.js create mode 100644 frontend/src/Album/Delete/DeleteAlbumModal.js create mode 100644 frontend/src/Album/Delete/DeleteAlbumModalContent.css create mode 100644 frontend/src/Album/Delete/DeleteAlbumModalContent.js create mode 100644 frontend/src/Album/Delete/DeleteAlbumModalContentConnector.js create mode 100644 frontend/src/Helpers/Props/metadataProfileNames.js rename frontend/src/{AddArtist/AddNewArtist/AddNewArtist.css => Search/AddNewItem.css} (100%) rename frontend/src/{AddArtist/AddNewArtist/AddNewArtist.js => Search/AddNewItem.js} (62%) rename frontend/src/{AddArtist/AddNewArtist/AddNewArtistConnector.js => Search/AddNewItemConnector.js} (52%) create mode 100644 frontend/src/Search/Album/AddNewAlbumModal.js create mode 100644 frontend/src/Search/Album/AddNewAlbumModalContent.css create mode 100644 frontend/src/Search/Album/AddNewAlbumModalContent.js create mode 100644 frontend/src/Search/Album/AddNewAlbumModalContentConnector.js create mode 100644 frontend/src/Search/Album/AddNewAlbumSearchResult.css create mode 100644 frontend/src/Search/Album/AddNewAlbumSearchResult.js create mode 100644 frontend/src/Search/Album/AddNewAlbumSearchResultConnector.js rename frontend/src/{AddArtist/AddNewArtist => Search/Artist}/AddNewArtistModal.js (100%) rename frontend/src/{AddArtist/AddNewArtist => Search/Artist}/AddNewArtistModalContent.css (86%) create mode 100644 frontend/src/Search/Artist/AddNewArtistModalContent.js rename frontend/src/{AddArtist/AddNewArtist => Search/Artist}/AddNewArtistModalContentConnector.js (85%) rename frontend/src/{AddArtist/AddNewArtist => Search/Artist}/AddNewArtistSearchResult.css (100%) rename frontend/src/{AddArtist/AddNewArtist => Search/Artist}/AddNewArtistSearchResult.js (99%) rename frontend/src/{AddArtist/AddNewArtist => Search/Artist}/AddNewArtistSearchResultConnector.js (100%) create mode 100644 frontend/src/Search/Common/AddArtistOptionsForm.css create mode 100644 frontend/src/Search/Common/AddArtistOptionsForm.js rename frontend/src/Store/Actions/{addArtistActions.js => searchActions.js} (60%) create mode 100644 frontend/src/Utilities/Album/getNewAlbum.js create mode 100644 src/Lidarr.Api.V1/Search/SearchModule.cs create mode 100644 src/Lidarr.Api.V1/Search/SearchResource.cs create mode 100644 src/NzbDrone.Core.Test/MusicTests/AddAlbumFixture.cs create mode 100644 src/NzbDrone.Core/MetadataSource/ISearchForNewEntity.cs create mode 100644 src/NzbDrone.Core/MetadataSource/SkyHook/Resource/EntityResource.cs delete mode 100644 src/NzbDrone.Core/Music/ArtistNameNormalizer.cs create mode 100644 src/NzbDrone.Core/Music/Events/AlbumUpdatedEvent.cs create mode 100644 src/NzbDrone.Core/Music/Handlers/AlbumAddedHandler.cs rename src/NzbDrone.Core/Music/{ => Handlers}/ArtistAddedHandler.cs (84%) rename src/NzbDrone.Core/Music/{ => Handlers}/ArtistScannedHandler.cs (64%) create mode 100644 src/NzbDrone.Core/Music/Model/AddAlbumOptions.cs rename src/NzbDrone.Core/Music/{ => Model}/AddArtistOptions.cs (64%) rename src/NzbDrone.Core/Music/{ => Model}/Album.cs (88%) rename src/NzbDrone.Core/Music/{ => Model}/Artist.cs (82%) rename src/NzbDrone.Core/Music/{ => Model}/ArtistMetadata.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/ArtistStatusType.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/Entity.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/Links.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/Medium.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/Member.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/MonitoringOptions.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/PrimaryAlbumType.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/Ratings.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/Release.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/ReleaseStatus.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/SecondaryAlbumType.cs (100%) rename src/NzbDrone.Core/Music/{ => Model}/Track.cs (100%) rename src/NzbDrone.Core/Music/{ => Repositories}/AlbumRepository.cs (98%) rename src/NzbDrone.Core/Music/{ => Repositories}/ArtistMetadataRepository.cs (97%) rename src/NzbDrone.Core/Music/{ => Repositories}/ArtistRepository.cs (97%) rename src/NzbDrone.Core/Music/{ => Repositories}/ReleaseRepository.cs (100%) rename src/NzbDrone.Core/Music/{ => Repositories}/TrackRepository.cs (96%) create mode 100644 src/NzbDrone.Core/Music/Services/AddAlbumService.cs rename src/NzbDrone.Core/Music/{ => Services}/AddArtistService.cs (85%) rename src/NzbDrone.Core/Music/{ => Services}/AlbumAddedService.cs (78%) rename src/NzbDrone.Core/Music/{ => Services}/AlbumCutoffService.cs (91%) rename src/NzbDrone.Core/Music/{ => Services}/AlbumEditedService.cs (94%) rename src/NzbDrone.Core/Music/{ => Services}/AlbumMonitoredService.cs (94%) rename src/NzbDrone.Core/Music/{ => Services}/AlbumService.cs (90%) rename src/NzbDrone.Core/Music/{ => Services}/ArtistEditedService.cs (100%) rename src/NzbDrone.Core/Music/{ => Services}/ArtistMetadataService.cs (84%) rename src/NzbDrone.Core/Music/{ => Services}/ArtistService.cs (90%) rename src/NzbDrone.Core/Music/{ => Services}/MoveArtistService.cs (99%) rename src/NzbDrone.Core/Music/{ => Services}/RefreshAlbumReleaseService.cs (94%) rename src/NzbDrone.Core/Music/{ => Services}/RefreshAlbumService.cs (96%) rename src/NzbDrone.Core/Music/{ => Services}/RefreshArtistService.cs (96%) rename src/NzbDrone.Core/Music/{ => Services}/RefreshEntityServiceBase.cs (96%) rename src/NzbDrone.Core/Music/{ => Services}/RefreshTrackService.cs (97%) rename src/NzbDrone.Core/Music/{ => Services}/ReleaseService.cs (100%) rename src/NzbDrone.Core/Music/{ => Services}/TrackService.cs (94%) rename src/NzbDrone.Core/Music/{ => Utilities}/AddArtistValidator.cs (100%) rename src/NzbDrone.Core/Music/{ => Utilities}/ArtistPathBuilder.cs (100%) rename src/NzbDrone.Core/Music/{ => Utilities}/ShouldRefreshAlbum.cs (100%) rename src/NzbDrone.Core/Music/{ => Utilities}/ShouldRefreshArtist.cs (97%) diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.js deleted file mode 100644 index 2278812b8..000000000 --- a/frontend/src/AddArtist/AddNewArtist/AddNewArtistModalContent.js +++ /dev/null @@ -1,241 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { Component } from 'react'; -import TextTruncate from 'react-text-truncate'; -import { icons, kinds, inputTypes, tooltipPositions } from 'Helpers/Props'; -import Icon from 'Components/Icon'; -import SpinnerButton from 'Components/Link/SpinnerButton'; -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 CheckInput from 'Components/Form/CheckInput'; -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 Popover from 'Components/Tooltip/Popover'; -import ArtistPoster from 'Artist/ArtistPoster'; -import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent'; -import styles from './AddNewArtistModalContent.css'; - -class AddNewArtistModalContent extends Component { - - // - // Lifecycle - - constructor(props, context) { - super(props, context); - - this.state = { - searchForMissingAlbums: false - }; - } - - // - // Listeners - - onSearchForMissingAlbumsChange = ({ value }) => { - this.setState({ searchForMissingAlbums: value }); - } - - onQualityProfileIdChange = ({ value }) => { - this.props.onInputChange({ name: 'qualityProfileId', value: parseInt(value) }); - } - - onMetadataProfileIdChange = ({ value }) => { - this.props.onInputChange({ name: 'metadataProfileId', value: parseInt(value) }); - } - - onAddArtistPress = () => { - this.props.onAddArtistPress(this.state.searchForMissingAlbums); - } - - // - // Render - - render() { - const { - artistName, - overview, - images, - isAdding, - rootFolderPath, - monitor, - qualityProfileId, - metadataProfileId, - albumFolder, - tags, - showMetadataProfile, - isSmallScreen, - onModalClose, - onInputChange, - ...otherProps - } = this.props; - - return ( - - - {artistName} - - - -
- { - isSmallScreen ? - null: -
- -
- } - -
- { - overview ? -
- -
: - null - } - -
- - Root Folder - - - - - - - Monitor - - - } - title="Monitoring Options" - body={} - position={tooltipPositions.RIGHT} - /> - - - - - - - Quality Profile - - - - - - Metadata Profile - - - - - - Album Folder - - - - - - Tags - - - -
-
-
-
- - - - - - Add {artistName} - - -
- ); - } -} - -AddNewArtistModalContent.propTypes = { - artistName: PropTypes.string.isRequired, - overview: PropTypes.string, - images: PropTypes.arrayOf(PropTypes.object).isRequired, - isAdding: PropTypes.bool.isRequired, - addError: PropTypes.object, - rootFolderPath: PropTypes.object, - monitor: PropTypes.object.isRequired, - qualityProfileId: PropTypes.object, - metadataProfileId: PropTypes.object, - albumFolder: PropTypes.object.isRequired, - tags: PropTypes.object.isRequired, - showMetadataProfile: PropTypes.bool.isRequired, - isSmallScreen: PropTypes.bool.isRequired, - onModalClose: PropTypes.func.isRequired, - onInputChange: PropTypes.func.isRequired, - onAddArtistPress: PropTypes.func.isRequired -}; - -export default AddNewArtistModalContent; diff --git a/frontend/src/AddArtist/ArtistMetadataProfilePopoverContent.js b/frontend/src/AddArtist/ArtistMetadataProfilePopoverContent.js new file mode 100644 index 000000000..134f1eb1f --- /dev/null +++ b/frontend/src/AddArtist/ArtistMetadataProfilePopoverContent.js @@ -0,0 +1,11 @@ +import React from 'react'; + +function ArtistMetadataProfilePopoverContent() { + return ( +
+ Select 'None' to only include items manually added via search +
+ ); +} + +export default ArtistMetadataProfilePopoverContent; diff --git a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistConnector.js b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistConnector.js index 4ce182bbd..65bcc9645 100644 --- a/frontend/src/AddArtist/ImportArtist/Import/ImportArtistConnector.js +++ b/frontend/src/AddArtist/ImportArtist/Import/ImportArtistConnector.js @@ -6,7 +6,7 @@ import { connect } from 'react-redux'; import { createSelector } from 'reselect'; import { setImportArtistValue, importArtist, clearImportArtist } from 'Store/Actions/importArtistActions'; import { fetchRootFolders } from 'Store/Actions/rootFolderActions'; -import { setAddArtistDefault } from 'Store/Actions/addArtistActions'; +import { setAddDefault } from 'Store/Actions/searchActions'; import createRouteMatchShape from 'Helpers/Props/Shapes/createRouteMatchShape'; import ImportArtist from './ImportArtist'; @@ -67,7 +67,7 @@ const mapDispatchToProps = { dispatchImportArtist: importArtist, dispatchClearImportArtist: clearImportArtist, dispatchFetchRootFolders: fetchRootFolders, - dispatchSetAddArtistDefault: setAddArtistDefault + dispatchSetAddDefault: setAddDefault }; class ImportArtistConnector extends Component { @@ -82,7 +82,7 @@ class ImportArtistConnector extends Component { defaultQualityProfileId, defaultMetadataProfileId, dispatchFetchRootFolders, - dispatchSetAddArtistDefault + dispatchSetAddDefault } = this.props; if (!this.props.rootFoldersPopulated) { @@ -109,7 +109,7 @@ class ImportArtistConnector extends Component { } if (setDefaults) { - dispatchSetAddArtistDefault(setDefaultPayload); + dispatchSetAddDefault(setDefaultPayload); } } @@ -121,7 +121,7 @@ class ImportArtistConnector extends Component { // Listeners onInputChange = (ids, name, value) => { - this.props.dispatchSetAddArtistDefault({ [name]: value }); + this.props.dispatchSetAddDefault({ [name]: value }); ids.forEach((id) => { this.props.dispatchSetImportArtistValue({ @@ -164,7 +164,7 @@ ImportArtistConnector.propTypes = { dispatchImportArtist: PropTypes.func.isRequired, dispatchClearImportArtist: PropTypes.func.isRequired, dispatchFetchRootFolders: PropTypes.func.isRequired, - dispatchSetAddArtistDefault: PropTypes.func.isRequired + dispatchSetAddDefault: PropTypes.func.isRequired }; export default connect(createMapStateToProps, mapDispatchToProps)(ImportArtistConnector); diff --git a/frontend/src/Album/Delete/DeleteAlbumModal.js b/frontend/src/Album/Delete/DeleteAlbumModal.js new file mode 100644 index 000000000..a18644d64 --- /dev/null +++ b/frontend/src/Album/Delete/DeleteAlbumModal.js @@ -0,0 +1,33 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { sizes } from 'Helpers/Props'; +import Modal from 'Components/Modal/Modal'; +import DeleteAlbumModalContentConnector from './DeleteAlbumModalContentConnector'; + +function DeleteAlbumModal(props) { + const { + isOpen, + onModalClose, + ...otherProps + } = props; + + return ( + + + + ); +} + +DeleteAlbumModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +export default DeleteAlbumModal; diff --git a/frontend/src/Album/Delete/DeleteAlbumModalContent.css b/frontend/src/Album/Delete/DeleteAlbumModalContent.css new file mode 100644 index 000000000..dbfef0871 --- /dev/null +++ b/frontend/src/Album/Delete/DeleteAlbumModalContent.css @@ -0,0 +1,12 @@ +.pathContainer { + margin-bottom: 20px; +} + +.pathIcon { + margin-right: 8px; +} + +.deleteFilesMessage { + margin-top: 20px; + color: $dangerColor; +} diff --git a/frontend/src/Album/Delete/DeleteAlbumModalContent.js b/frontend/src/Album/Delete/DeleteAlbumModalContent.js new file mode 100644 index 000000000..b6bd215fe --- /dev/null +++ b/frontend/src/Album/Delete/DeleteAlbumModalContent.js @@ -0,0 +1,157 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import formatBytes from 'Utilities/Number/formatBytes'; +import { inputTypes, kinds } from 'Helpers/Props'; +import Button from 'Components/Link/Button'; +import FormGroup from 'Components/Form/FormGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import styles from './DeleteAlbumModalContent.css'; + +class DeleteAlbumModalContent extends Component { + + // + // Lifecycle + + constructor(props, context) { + super(props, context); + + this.state = { + deleteFiles: false, + addImportListExclusion: true + }; + } + + // + // Listeners + + onDeleteFilesChange = ({ value }) => { + this.setState({ deleteFiles: value }); + } + + onAddImportListExclusionChange = ({ value }) => { + this.setState({ addImportListExclusion: value }); + } + + onDeleteAlbumConfirmed = () => { + const deleteFiles = this.state.deleteFiles; + const addImportListExclusion = this.state.addImportListExclusion; + + this.setState({ deleteFiles: false }); + this.setState({ addImportListExclusion: false }); + this.props.onDeletePress(deleteFiles, addImportListExclusion); + } + + // + // Render + + render() { + const { + title, + statistics, + onModalClose + } = this.props; + + const { + trackFileCount, + sizeOnDisk + } = statistics; + + const deleteFiles = this.state.deleteFiles; + const addImportListExclusion = this.state.addImportListExclusion; + + const deleteFilesLabel = `Delete ${trackFileCount} Track Files`; + const deleteFilesHelpText = 'Delete the track files'; + + return ( + + + Delete - {title} + + + + + + {deleteFilesLabel} + + + + + + Add List Exclusion + + + + + { + !addImportListExclusion && +
+
If you don't add an import list exclusion and the artist has a metadata profile other than 'None' then this album may be re-added during the next artist refresh.
+
+ } + + { + deleteFiles && +
+
The album's files will be deleted.
+ + { + !!trackFileCount && +
{trackFileCount} track files totaling {formatBytes(sizeOnDisk)}
+ } +
+ } + +
+ + + + + + +
+ ); + } +} + +DeleteAlbumModalContent.propTypes = { + title: PropTypes.string.isRequired, + statistics: PropTypes.object.isRequired, + onDeletePress: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired +}; + +DeleteAlbumModalContent.defaultProps = { + statistics: { + trackFileCount: 0 + } +}; + +export default DeleteAlbumModalContent; diff --git a/frontend/src/Album/Delete/DeleteAlbumModalContentConnector.js b/frontend/src/Album/Delete/DeleteAlbumModalContentConnector.js new file mode 100644 index 000000000..f31b4ac65 --- /dev/null +++ b/frontend/src/Album/Delete/DeleteAlbumModalContentConnector.js @@ -0,0 +1,62 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { createSelector } from 'reselect'; +import { push } from 'connected-react-router'; +import createAlbumSelector from 'Store/Selectors/createAlbumSelector'; +import { deleteAlbum } from 'Store/Actions/albumActions'; +import DeleteAlbumModalContent from './DeleteAlbumModalContent'; + +function createMapStateToProps() { + return createSelector( + createAlbumSelector(), + (album) => { + return album; + } + ); +} + +const mapDispatchToProps = { + push, + deleteAlbum +}; + +class DeleteAlbumModalContentConnector extends Component { + + // + // Listeners + + onDeletePress = (deleteFiles, addImportListExclusion) => { + this.props.deleteAlbum({ + id: this.props.albumId, + deleteFiles, + addImportListExclusion + }); + + this.props.onModalClose(true); + + this.props.push(`${window.Lidarr.urlBase}/artist/${this.props.foreignArtistId}`); + } + + // + // Render + + render() { + return ( + + ); + } +} + +DeleteAlbumModalContentConnector.propTypes = { + albumId: PropTypes.number.isRequired, + foreignArtistId: PropTypes.string.isRequired, + push: PropTypes.func.isRequired, + onModalClose: PropTypes.func.isRequired, + deleteAlbum: PropTypes.func.isRequired +}; + +export default connect(createMapStateToProps, mapDispatchToProps)(DeleteAlbumModalContentConnector); diff --git a/frontend/src/Album/Details/AlbumDetails.js b/frontend/src/Album/Details/AlbumDetails.js index f2288b00f..857f47e1d 100644 --- a/frontend/src/Album/Details/AlbumDetails.js +++ b/frontend/src/Album/Details/AlbumDetails.js @@ -18,6 +18,7 @@ import AlbumCover from 'Album/AlbumCover'; import OrganizePreviewModalConnector from 'Organize/OrganizePreviewModalConnector'; import RetagPreviewModalConnector from 'Retag/RetagPreviewModalConnector'; import EditAlbumModalConnector from 'Album/Edit/EditAlbumModalConnector'; +import DeleteAlbumModal from 'Album/Delete/DeleteAlbumModal'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import PageContent from 'Components/Page/PageContent'; import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector'; @@ -88,6 +89,7 @@ class AlbumDetails extends Component { isInteractiveSearchModalOpen: false, isManageTracksOpen: false, isEditAlbumModalOpen: false, + isDeleteAlbumModalOpen: false, allExpanded: false, allCollapsed: false, expandedState: {} @@ -121,6 +123,17 @@ class AlbumDetails extends Component { this.setState({ isEditAlbumModalOpen: false }); } + onDeleteAlbumPress = () => { + this.setState({ + isEditAlbumModalOpen: false, + isDeleteAlbumModalOpen: true + }); + } + + onDeleteAlbumModalClose = () => { + this.setState({ isDeleteAlbumModalOpen: false }); + } + onManageTracksPress = () => { this.setState({ isManageTracksOpen: true }); } @@ -208,6 +221,7 @@ class AlbumDetails extends Component { isArtistHistoryModalOpen, isInteractiveSearchModalOpen, isEditAlbumModalOpen, + isDeleteAlbumModalOpen, isManageTracksOpen, allExpanded, allCollapsed, @@ -276,6 +290,12 @@ class AlbumDetails extends Component { onPress={this.onEditAlbumPress} /> + + + + diff --git a/frontend/src/Album/Details/AlbumDetailsPageConnector.js b/frontend/src/Album/Details/AlbumDetailsPageConnector.js index fffd014ad..320348b9b 100644 --- a/frontend/src/Album/Details/AlbumDetailsPageConnector.js +++ b/frontend/src/Album/Details/AlbumDetailsPageConnector.js @@ -1,3 +1,4 @@ +import _ from 'lodash'; import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { connect } from 'react-redux'; @@ -20,6 +21,18 @@ function createMapStateToProps() { const isFetching = albums.isFetching || artist.isFetching; const isPopulated = albums.isPopulated && artist.isPopulated; + // if albums have been fetched, make sure requested one exists + // otherwise don't map foreignAlbumId to trigger not found page + if (!isFetching && isPopulated) { + const albumIndex = _.findIndex(albums.items, { foreignAlbumId }); + if (albumIndex === -1) { + return { + isFetching, + isPopulated + }; + } + } + return { foreignAlbumId, isFetching, diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js index ed55547e0..19c4785b1 100644 --- a/frontend/src/App/AppRoutes.js +++ b/frontend/src/App/AppRoutes.js @@ -5,7 +5,7 @@ import getPathWithUrlBase from 'Utilities/getPathWithUrlBase'; import NotFound from 'Components/NotFound'; import Switch from 'Components/Router/Switch'; import ArtistIndexConnector from 'Artist/Index/ArtistIndexConnector'; -import AddNewArtistConnector from 'AddArtist/AddNewArtist/AddNewArtistConnector'; +import AddNewItemConnector from 'Search/AddNewItemConnector'; import ImportArtist from 'AddArtist/ImportArtist/ImportArtist'; import ArtistEditorConnector from 'Artist/Editor/ArtistEditorConnector'; import AlbumStudioConnector from 'AlbumStudio/AlbumStudioConnector'; @@ -72,8 +72,8 @@ function AppRoutes(props) { }
- Missing Albums, Singles, or Other Types? Modify or Create a New Metadata Profile! + Missing Albums, Singles, or Other Types? Modify or create a new + Metadata Profile + or manually + Search + for new items!
e.monitored); + const albumTypes = _.uniq(_.map(items, 'albumType')); return { isAlbumsFetching: isFetching, isAlbumsPopulated: isPopulated, albumsError: error, hasAlbums, - hasMonitoredAlbums + hasMonitoredAlbums, + albumTypes }; } ); @@ -65,20 +67,12 @@ function createMapStateToProps() { (state, { foreignArtistId }) => foreignArtistId, selectAlbums, selectTrackFiles, - (state) => state.settings.metadataProfiles, createAllArtistSelector(), createCommandsSelector(), - (foreignArtistId, albums, trackFiles, metadataProfiles, allArtists, commands) => { + (foreignArtistId, albums, trackFiles, allArtists, commands) => { const sortedArtist = _.orderBy(allArtists, 'sortName'); const artistIndex = _.findIndex(sortedArtist, { foreignArtistId }); const artist = sortedArtist[artistIndex]; - const metadataProfile = _.find(metadataProfiles.items, { id: artist.metadataProfileId }); - const albumTypes = _.reduce(metadataProfile.primaryAlbumTypes, (acc, primaryType) => { - if (primaryType.allowed) { - acc.push(primaryType.albumType.name); - } - return acc; - }, []); if (!artist) { return {}; @@ -89,7 +83,8 @@ function createMapStateToProps() { isAlbumsPopulated, albumsError, hasAlbums, - hasMonitoredAlbums + hasMonitoredAlbums, + albumTypes } = albums; const { diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.css b/frontend/src/Artist/Edit/EditArtistModalContent.css index a2b6014df..fd7ddf093 100644 --- a/frontend/src/Artist/Edit/EditArtistModalContent.css +++ b/frontend/src/Artist/Edit/EditArtistModalContent.css @@ -3,3 +3,7 @@ margin-right: auto; } + +.labelIcon { + margin-left: 8px; +} diff --git a/frontend/src/Artist/Edit/EditArtistModalContent.js b/frontend/src/Artist/Edit/EditArtistModalContent.js index 73dd652e8..adeb61c1e 100644 --- a/frontend/src/Artist/Edit/EditArtistModalContent.js +++ b/frontend/src/Artist/Edit/EditArtistModalContent.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { inputTypes, kinds } from 'Helpers/Props'; +import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; import Button from 'Components/Link/Button'; import SpinnerButton from 'Components/Link/SpinnerButton'; import ModalContent from 'Components/Modal/ModalContent'; @@ -11,7 +11,10 @@ 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 Icon from 'Components/Icon'; +import Popover from 'Components/Tooltip/Popover'; import MoveArtistModal from 'Artist/MoveArtist/MoveArtistModal'; +import ArtistMetadataProfilePopoverContent from 'AddArtist/ArtistMetadataProfilePopoverContent'; import styles from './EditArtistModalContent.css'; class EditArtistModalContent extends Component { @@ -122,12 +125,28 @@ class EditArtistModalContent extends Component { { showMetadataProfile && - Metadata Profile + + Metadata Profile + + + } + title="Metadata Profile" + body={} + position={tooltipPositions.RIGHT} + /> + + diff --git a/frontend/src/Artist/NoArtist.js b/frontend/src/Artist/NoArtist.js index b869a8d58..1db9f41db 100644 --- a/frontend/src/Artist/NoArtist.js +++ b/frontend/src/Artist/NoArtist.js @@ -20,7 +20,7 @@ function NoArtist(props) { return (
- No artist found, to get started you'll want to add a new artist or import some existing ones. + No artist found, to get started you'll want to add a new artist or album or import some existing ones.
@@ -34,7 +34,7 @@ function NoArtist(props) {