From 72267d3cb411dfe46a3d6013a00a0a2f50e708b3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Thu, 26 Jan 2023 20:26:12 -0800 Subject: [PATCH] New: Album Studio is now part of artists list (cherry picked from commit bdcfef80d627e777d7932c54cda04cbe7c656ffc) Load albums for album details on mouse hover --- frontend/src/Album/Album.ts | 4 + frontend/src/App/State/AlbumAppState.ts | 8 ++ frontend/src/App/State/AppState.ts | 2 + frontend/src/Artist/Artist.ts | 2 +- .../Index/Select/AlbumStudio/AlbumDetails.css | 10 ++ .../Select/AlbumStudio/AlbumDetails.css.d.ts | 8 ++ .../Index/Select/AlbumStudio/AlbumDetails.tsx | 88 +++++++++++++++++ .../Select/AlbumStudio/AlbumStudioAlbum.css | 39 ++++++++ .../AlbumStudio/AlbumStudioAlbum.css.d.ts | 12 +++ .../Select/AlbumStudio/AlbumStudioAlbum.tsx | 83 ++++++++++++++++ .../AlbumStudio/ChangeMonitoringModal.tsx | 26 +++++ .../ChangeMonitoringModalContent.css | 26 +++++ .../ChangeMonitoringModalContent.css.d.ts | 10 ++ .../ChangeMonitoringModalContent.tsx | 96 +++++++++++++++++++ .../Index/Select/ArtistIndexSelectFooter.css | 4 + .../Index/Select/ArtistIndexSelectFooter.tsx | 50 +++++++++- .../src/Artist/Index/Table/AlbumsCell.css | 4 + .../Artist/Index/Table/AlbumsCell.css.d.ts | 7 ++ .../src/Artist/Index/Table/AlbumsCell.tsx | 38 ++++++++ .../src/Artist/Index/Table/ArtistIndexRow.tsx | 12 ++- .../Artist/Index/Table/ArtistStatusCell.tsx | 30 ++++-- .../MonitoringOptionsModalContentConnector.js | 3 +- frontend/src/Store/Actions/artistActions.js | 7 +- .../Selectors/createArtistAlbumsSelector.ts | 27 ++++++ src/NzbDrone.Core/Localization/Core/en.json | 13 ++- 25 files changed, 591 insertions(+), 18 deletions(-) create mode 100644 frontend/src/App/State/AlbumAppState.ts create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css.d.ts create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css.d.ts create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModal.tsx create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css.d.ts create mode 100644 frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx create mode 100644 frontend/src/Artist/Index/Table/AlbumsCell.css create mode 100644 frontend/src/Artist/Index/Table/AlbumsCell.css.d.ts create mode 100644 frontend/src/Artist/Index/Table/AlbumsCell.tsx create mode 100644 frontend/src/Store/Selectors/createArtistAlbumsSelector.ts diff --git a/frontend/src/Album/Album.ts b/frontend/src/Album/Album.ts index 03a129e06..c9f10a87c 100644 --- a/frontend/src/Album/Album.ts +++ b/frontend/src/Album/Album.ts @@ -1,4 +1,5 @@ import ModelBase from 'App/ModelBase'; +import Artist from 'Artist/Artist'; export interface Statistics { trackCount: number; @@ -9,13 +10,16 @@ export interface Statistics { } interface Album extends ModelBase { + artist: Artist; foreignAlbumId: string; title: string; overview: string; disambiguation?: string; + albumType: string; monitored: boolean; releaseDate: string; statistics: Statistics; + isSaving?: boolean; } export default Album; diff --git a/frontend/src/App/State/AlbumAppState.ts b/frontend/src/App/State/AlbumAppState.ts new file mode 100644 index 000000000..e03d4a497 --- /dev/null +++ b/frontend/src/App/State/AlbumAppState.ts @@ -0,0 +1,8 @@ +import Album from 'Album/Album'; +import AppSectionState, { + AppSectionDeleteState, +} from 'App/State/AppSectionState'; + +interface AlbumAppState extends AppSectionState, AppSectionDeleteState {} + +export default AlbumAppState; diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts index e6b9d596c..d1de99887 100644 --- a/frontend/src/App/State/AppState.ts +++ b/frontend/src/App/State/AppState.ts @@ -1,3 +1,4 @@ +import AlbumAppState from './AlbumAppState'; import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState'; import SettingsAppState from './SettingsAppState'; import TagsAppState from './TagsAppState'; @@ -35,6 +36,7 @@ export interface CustomFilter { } interface AppState { + albums: AlbumAppState; artist: ArtistAppState; artistIndex: ArtistIndexAppState; settings: SettingsAppState; diff --git a/frontend/src/Artist/Artist.ts b/frontend/src/Artist/Artist.ts index 31b609d73..61266a8d4 100644 --- a/frontend/src/Artist/Artist.ts +++ b/frontend/src/Artist/Artist.ts @@ -23,6 +23,7 @@ export interface Ratings { interface Artist extends ModelBase { added: string; + artistMetadataId: string; foreignArtistId: string; cleanName: string; ended: boolean; @@ -37,7 +38,6 @@ interface Artist extends ModelBase { metadataProfileId: number; ratings: Ratings; rootFolderPath: string; - albums: Album[]; sortName: string; statistics: Statistics; status: string; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css new file mode 100644 index 000000000..5f6ee37c1 --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css @@ -0,0 +1,10 @@ +.albums { + display: flex; + flex-wrap: wrap; +} + +.truncated { + align-self: center; + flex: 0 0 100%; + padding: 4px 6px; +} diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css.d.ts b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css.d.ts new file mode 100644 index 000000000..13c576f2c --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.css.d.ts @@ -0,0 +1,8 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'albums': string; + 'truncated': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx new file mode 100644 index 000000000..bd6a01e10 --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumDetails.tsx @@ -0,0 +1,88 @@ +import _ from 'lodash'; +import React, { useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import Alert from 'Components/Alert'; +import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import { kinds } from 'Helpers/Props'; +import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions'; +import createArtistAlbumsSelector from 'Store/Selectors/createArtistAlbumsSelector'; +import translate from 'Utilities/String/translate'; +import AlbumStudioAlbum from './AlbumStudioAlbum'; +import styles from './AlbumDetails.css'; + +interface AlbumDetailsProps { + artistId: number; +} + +function AlbumDetails(props: AlbumDetailsProps) { + const { artistId } = props; + + const { + isFetching, + isPopulated, + error, + items: albums, + } = useSelector(createArtistAlbumsSelector(artistId)); + + const dispatch = useDispatch(); + + useEffect(() => { + dispatch(fetchAlbums({ artistId })); + + return () => { + dispatch(clearAlbums()); + }; + }, [dispatch, artistId]); + + const latestAlbums = useMemo(() => { + const sortedAlbums = _.orderBy(albums, 'releaseDate', 'desc'); + + return sortedAlbums.slice(0, 20); + }, [albums]); + + return ( +
+ {isFetching ? : null} + + {!isFetching && error ? ( + {translate('AlbumsLoadError')} + ) : null} + + {isPopulated && !error + ? latestAlbums.map((album) => { + const { + id: albumId, + title, + disambiguation, + albumType, + monitored, + statistics, + isSaving, + } = album; + + return ( + + ); + }) + : null} + + {latestAlbums.length < albums.length ? ( +
+ {translate('AlbumStudioTruncated')} +
+ ) : null} +
+ ); +} + +export default AlbumDetails; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css new file mode 100644 index 000000000..c568a2489 --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css @@ -0,0 +1,39 @@ +.album { + display: flex; + align-items: stretch; + overflow: hidden; + margin: 2px 4px; + border: 1px solid var(--borderColor); + border-radius: 4px; + background-color: var(--albumBackgroundColor); + cursor: default; +} + +.info { + padding: 0 4px; +} + +.albumType { + padding: 0 4px; + border-width: 0 1px; + border-style: solid; + border-color: var(--borderColor); + background-color: var(--albumBackgroundColor); + color: var(--defaultColor); +} + +.tracks { + padding: 0 4px; + background-color: var(--trackBackgroundColor); + color: var(--defaultColor); +} + +.allTracks { + background-color: color(#27c24c saturation(-25%)); + color: var(--white); +} + +.missingWanted { + background-color: color(#f05050 saturation(-20%)); + color: var(--white); +} diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css.d.ts b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css.d.ts new file mode 100644 index 000000000..31142374e --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.css.d.ts @@ -0,0 +1,12 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'album': string; + 'albumType': string; + 'allTracks': string; + 'info': string; + 'missingWanted': string; + 'tracks': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx new file mode 100644 index 000000000..f7e133a84 --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/AlbumStudioAlbum.tsx @@ -0,0 +1,83 @@ +import classNames from 'classnames'; +import React, { useCallback } from 'react'; +import { useDispatch } from 'react-redux'; +import { Statistics } from 'Album/Album'; +import MonitorToggleButton from 'Components/MonitorToggleButton'; +import { toggleAlbumsMonitored } from 'Store/Actions/albumActions'; +import translate from 'Utilities/String/translate'; +import styles from './AlbumStudioAlbum.css'; + +interface AlbumStudioAlbumProps { + artistId: number; + albumId: number; + title: string; + disambiguation: string; + albumType: string; + monitored: boolean; + statistics: Statistics; + isSaving: boolean; +} + +function AlbumStudioAlbum(props: AlbumStudioAlbumProps) { + const { + albumId, + title, + disambiguation, + albumType, + monitored, + statistics = { + trackFileCount: 0, + totalTrackCount: 0, + percentOfTracks: 0, + }, + isSaving = false, + } = props; + + const { trackFileCount, totalTrackCount, percentOfTracks } = statistics; + + const dispatch = useDispatch(); + const onAlbumMonitoredPress = useCallback(() => { + dispatch( + toggleAlbumsMonitored({ + albumIds: [albumId], + monitored: !monitored, + }) + ); + }, [albumId, monitored, dispatch]); + + return ( +
+
+ + + + {disambiguation ? `${title} (${disambiguation})` : `${title}`} + +
+ +
+ {albumType} +
+ +
+ {totalTrackCount === 0 ? '0/0' : `${trackFileCount}/${totalTrackCount}`} +
+
+ ); +} + +export default AlbumStudioAlbum; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModal.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModal.tsx new file mode 100644 index 000000000..b48717af0 --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModal.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Modal from 'Components/Modal/Modal'; +import ChangeMonitoringModalContent from './ChangeMonitoringModalContent'; + +interface ChangeMonitoringModalProps { + isOpen: boolean; + artistIds: number[]; + onSavePress(monitor: string): void; + onModalClose(): void; +} + +function ChangeMonitoringModal(props: ChangeMonitoringModalProps) { + const { isOpen, artistIds, onSavePress, onModalClose } = props; + + return ( + + + + ); +} + +export default ChangeMonitoringModal; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css new file mode 100644 index 000000000..29dc69dc4 --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css @@ -0,0 +1,26 @@ +.labelIcon { + margin-left: 8px; +} + +.message { + composes: alert from '~Components/Alert.css'; + + margin-bottom: 30px; +} + +.modalFooter { + composes: modalFooter from '~Components/Modal/ModalFooter.css'; + + justify-content: space-between; +} + +.selected { + font-weight: bold; +} + +@media only screen and (max-width: $breakpointExtraSmall) { + .modalFooter { + flex-direction: column; + gap: 10px; + } +} diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css.d.ts b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css.d.ts new file mode 100644 index 000000000..4c59f6545 --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.css.d.ts @@ -0,0 +1,10 @@ +// This file is automatically generated. +// Please do not change this file! +interface CssExports { + 'labelIcon': string; + 'message': string; + 'modalFooter': string; + 'selected': string; +} +export const cssExports: CssExports; +export default cssExports; diff --git a/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx new file mode 100644 index 000000000..c21c9a8af --- /dev/null +++ b/frontend/src/Artist/Index/Select/AlbumStudio/ChangeMonitoringModalContent.tsx @@ -0,0 +1,96 @@ +import React, { useCallback, useState } from 'react'; +import ArtistMonitoringOptionsPopoverContent from 'AddArtist/ArtistMonitoringOptionsPopoverContent'; +import Alert from 'Components/Alert'; +import Form from 'Components/Form/Form'; +import FormGroup from 'Components/Form/FormGroup'; +import FormInputGroup from 'Components/Form/FormInputGroup'; +import FormLabel from 'Components/Form/FormLabel'; +import Icon from 'Components/Icon'; +import Button from 'Components/Link/Button'; +import ModalBody from 'Components/Modal/ModalBody'; +import ModalContent from 'Components/Modal/ModalContent'; +import ModalFooter from 'Components/Modal/ModalFooter'; +import ModalHeader from 'Components/Modal/ModalHeader'; +import Popover from 'Components/Tooltip/Popover'; +import { icons, inputTypes, kinds, tooltipPositions } from 'Helpers/Props'; +import translate from 'Utilities/String/translate'; +import styles from './ChangeMonitoringModalContent.css'; + +const NO_CHANGE = 'noChange'; + +interface ChangeMonitoringModalContentProps { + artistIds: number[]; + saveError?: object; + onSavePress(monitor: string): void; + onModalClose(): void; +} + +function ChangeMonitoringModalContent( + props: ChangeMonitoringModalContentProps +) { + const { artistIds, onSavePress, onModalClose, ...otherProps } = props; + + const [monitor, setMonitor] = useState(NO_CHANGE); + + const onInputChange = useCallback( + ({ value }) => { + setMonitor(value); + }, + [setMonitor] + ); + + const onSavePressWrapper = useCallback(() => { + onSavePress(monitor); + }, [monitor, onSavePress]); + + const selectedCount = artistIds.length; + + return ( + + {translate('MonitorArtists')} + + + + {translate('MonitorAlbumExistingOnlyWarning')} + + +
+ + + {translate('MonitorExistingAlbums')} + + } + title={translate('MonitoringOptions')} + body={} + position={tooltipPositions.RIGHT} + /> + + + + +
+
+ + +
+ {translate('CountArtistsSelected', { count: selectedCount })} +
+ +
+ + + +
+
+
+ ); +} + +export default ChangeMonitoringModalContent; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css index b226a06a0..d385923ef 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.css @@ -51,6 +51,10 @@ gap: 20px; } + .actionButtons { + flex-wrap: wrap; + } + .actionButtons, .deleteButtons { display: flex; diff --git a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx index 108ce466b..c1500c7ac 100644 --- a/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx +++ b/frontend/src/Artist/Index/Select/ArtistIndexSelectFooter.tsx @@ -2,15 +2,20 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { createSelector } from 'reselect'; import { SelectActionType, useSelect } from 'App/SelectContext'; +import AppState from 'App/State/AppState'; import { RENAME_ARTIST, RETAG_ARTIST } from 'Commands/commandNames'; import SpinnerButton from 'Components/Link/SpinnerButton'; import PageContentFooter from 'Components/Page/PageContentFooter'; import { kinds } from 'Helpers/Props'; -import { saveArtistEditor } from 'Store/Actions/artistActions'; +import { + saveArtistEditor, + updateArtistsMonitor, +} from 'Store/Actions/artistActions'; import { fetchRootFolders } from 'Store/Actions/settingsActions'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import translate from 'Utilities/String/translate'; import getSelectedIds from 'Utilities/Table/getSelectedIds'; +import ChangeMonitoringModal from './AlbumStudio/ChangeMonitoringModal'; import RetagArtistModal from './AudioTags/RetagArtistModal'; import DeleteArtistModal from './Delete/DeleteArtistModal'; import EditArtistModal from './Edit/EditArtistModal'; @@ -19,7 +24,7 @@ import TagsModal from './Tags/TagsModal'; import styles from './ArtistIndexSelectFooter.css'; const artistEditorSelector = createSelector( - (state) => state.artist, + (state: AppState) => state.artist, (artist) => { const { isSaving, isDeleting, deleteError } = artist; @@ -48,9 +53,11 @@ function ArtistIndexSelectFooter() { const [isOrganizeModalOpen, setIsOrganizeModalOpen] = useState(false); const [isRetaggingModalOpen, setIsRetaggingModalOpen] = useState(false); const [isTagsModalOpen, setIsTagsModalOpen] = useState(false); + const [isMonitoringModalOpen, setIsMonitoringModalOpen] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [isSavingArtist, setIsSavingArtist] = useState(false); const [isSavingTags, setIsSavingTags] = useState(false); + const [isSavingMonitoring, setIsSavingMonitoring] = useState(false); const [selectState, selectDispatch] = useSelect(); const { selectedState } = selectState; @@ -124,6 +131,29 @@ function ArtistIndexSelectFooter() { [artistIds, dispatch] ); + const onMonitoringPress = useCallback(() => { + setIsMonitoringModalOpen(true); + }, [setIsMonitoringModalOpen]); + + const onMonitoringClose = useCallback(() => { + setIsMonitoringModalOpen(false); + }, [setIsMonitoringModalOpen]); + + const onMonitoringSavePress = useCallback( + (monitor: string) => { + setIsSavingMonitoring(true); + setIsMonitoringModalOpen(false); + + dispatch( + updateArtistsMonitor({ + artistIds, + monitor, + }) + ); + }, + [artistIds, dispatch] + ); + const onDeletePress = useCallback(() => { setIsDeleteModalOpen(true); }, [setIsDeleteModalOpen]); @@ -136,6 +166,7 @@ function ArtistIndexSelectFooter() { if (!isSaving) { setIsSavingArtist(false); setIsSavingTags(false); + setIsSavingMonitoring(false); } }, [isSaving]); @@ -188,6 +219,14 @@ function ArtistIndexSelectFooter() { > {translate('SetAppTags')} + + + {translate('UpdateMonitoring')} +
@@ -220,6 +259,13 @@ function ArtistIndexSelectFooter() { onModalClose={onTagsModalClose} /> + + + {isSelectMode && albumCount > 0 ? ( + } + position={TooltipPosition.Left} + canFlip={true} + /> + ) : ( + albumCount + )} + + ); +} + +export default AlbumsCell; diff --git a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx index 2508fe088..3f5b5385d 100644 --- a/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx +++ b/frontend/src/Artist/Index/Table/ArtistIndexRow.tsx @@ -26,6 +26,7 @@ import { executeCommand } from 'Store/Actions/commandActions'; import getProgressBarKind from 'Utilities/Artist/getProgressBarKind'; import formatBytes from 'Utilities/Number/formatBytes'; import translate from 'Utilities/String/translate'; +import AlbumsCell from './AlbumsCell'; import hasGrowableColumns from './hasGrowableColumns'; import selectTableOptions from './selectTableOptions'; import styles from './ArtistIndexRow.css'; @@ -164,6 +165,7 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { artistType={artistType} monitored={monitored} status={status} + isSelectMode={isSelectMode} isSaving={isSaving} component={VirtualTableRowCell} /> @@ -288,9 +290,13 @@ function ArtistIndexRow(props: ArtistIndexRowProps) { if (name === 'albumCount') { return ( - - {albumCount} - + ); } diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.tsx b/frontend/src/Artist/Index/Table/ArtistStatusCell.tsx index 05be1573e..00c7ae4c8 100644 --- a/frontend/src/Artist/Index/Table/ArtistStatusCell.tsx +++ b/frontend/src/Artist/Index/Table/ArtistStatusCell.tsx @@ -14,6 +14,7 @@ interface ArtistStatusCellProps { artistType?: string; monitored: boolean; status: string; + isSelectMode: boolean; isSaving: boolean; component?: React.ElementType; } @@ -25,12 +26,14 @@ function ArtistStatusCell(props: ArtistStatusCellProps) { artistType, monitored, status, + isSelectMode, isSaving, component: Component = VirtualTableRowCell, ...otherProps } = props; - const endedString = artistType === 'Person' ? 'Deceased' : 'Inactive'; + const endedString = + artistType === 'Person' ? translate('Deceased') : translate('Inactive'); const dispatch = useDispatch(); const onMonitoredPress = useCallback(() => { @@ -39,13 +42,24 @@ function ArtistStatusCell(props: ArtistStatusCellProps) { return ( - + {isSelectMode ? ( + + ) : ( + + )} { this.props.dispatchUpdateMonitoringOptions({ artistIds: [this.props.artistId], - monitor + monitor, + shouldFetchAlbumsAfterUpdate: true }); }; diff --git a/frontend/src/Store/Actions/artistActions.js b/frontend/src/Store/Actions/artistActions.js index 685b372ae..89384bcc4 100644 --- a/frontend/src/Store/Actions/artistActions.js +++ b/frontend/src/Store/Actions/artistActions.js @@ -358,7 +358,8 @@ export const actionHandlers = handleThunks({ artistIds, monitor, monitored, - monitorNewItems + monitorNewItems, + shouldFetchAlbumsAfterUpdate = false } = payload; const artists = []; @@ -390,7 +391,9 @@ export const actionHandlers = handleThunks({ }).request; promise.done((data) => { - dispatch(fetchAlbums({ artistId: artistIds[0] })); + if (shouldFetchAlbumsAfterUpdate) { + dispatch(fetchAlbums({ artistId: artistIds[0] })); + } dispatch(set({ section, diff --git a/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts new file mode 100644 index 000000000..2ae54a10c --- /dev/null +++ b/frontend/src/Store/Selectors/createArtistAlbumsSelector.ts @@ -0,0 +1,27 @@ +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import Artist from 'Artist/Artist'; +import { createArtistSelectorForHook } from './createArtistSelector'; + +function createArtistAlbumsSelector(artistId: number) { + return createSelector( + (state: AppState) => state.albums, + createArtistSelectorForHook(artistId), + (albums, artist = {} as Artist) => { + const { isFetching, isPopulated, error, items } = albums; + + const filteredAlbums = items.filter( + (album) => album.artist.artistMetadataId === artist.artistMetadataId + ); + + return { + isFetching, + isPopulated, + error, + items: filteredAlbums, + }; + } + ); +} + +export default createArtistAlbumsSelector; diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 62e303cf9..541b6436e 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -41,6 +41,7 @@ "AgeWhenGrabbed": "Age (when grabbed)", "Album": "Album", "AlbumCount": "Album Count", + "AlbumDetails": "Album Details", "AlbumHasNotAired": "Album has not aired", "AlbumIsDownloading": "Album is downloading", "AlbumIsDownloadingInterp": "Album is downloading - {0}% {1}", @@ -49,9 +50,12 @@ "AlbumReleaseDate": "Album Release Date", "AlbumStatus": "Album Status", "AlbumStudio": "Album Studio", + "AlbumStudioTracksDownloaded": "{trackFileCount}/{totalTrackCount} tracks downloaded", + "AlbumStudioTruncated": "Only latest 20 albums are shown, go to details to see all albums", "AlbumTitle": "Album Title", "AlbumType": "Album Type", "Albums": "Albums", + "AlbumsLoadError": "Unable to load albums", "All": "All", "AllAlbums": "All Albums", "AllAlbumsData": "Monitor all albums except specials", @@ -99,6 +103,8 @@ "ArtistClickToChangeAlbum": "Click to change album", "ArtistEditor": "Artist Editor", "ArtistFolderFormat": "Artist Folder Format", + "ArtistIsMonitored": "Artist is monitored", + "ArtistIsUnmonitored": "Artist is unmonitored", "ArtistMonitoring": "Artist Monitoring", "ArtistName": "Artist Name", "ArtistNameHelpText": "The name of the artist/album to exclude (can be anything meaningful)", @@ -193,7 +199,7 @@ "ContinuingNoAdditionalAlbumsAreExpected": "No additional albums are expected", "ContinuingOnly": "Continuing Only", "CopyToClipboard": "Copy to clipboard", - "CopyUsingHardlinksHelpText": "Hardlinks allow Lidarr to import seeding torrents to the the series folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume", + "CopyUsingHardlinksHelpText": "Hardlinks allow Lidarr to import seeding torrents to the the artist folder without taking extra disk space or copying the entire contents of the file. Hardlinks will only work if the source and destination are on the same volume", "CopyUsingHardlinksHelpTextWarning": "Occasionally, file locks may prevent renaming files that are being seeded. You may temporarily disable seeding and use Lidarr's rename function as a work around.", "CouldntFindAnyResultsForTerm": "Couldn't find any results for '{0}'", "CountAlbums": "{albumCount} albums", @@ -222,6 +228,7 @@ "Date": "Date", "DateAdded": "Date Added", "Dates": "Dates", + "Deceased": "Deceased", "DefaultDelayProfileHelpText": "This is the default profile. It applies to all artist that don't have an explicit profile.", "DefaultLidarrTags": "Default Lidarr Tags", "DefaultMetadataProfileIdHelpText": "Default Metadata Profile for artists detected in this folder", @@ -523,6 +530,7 @@ "ItsEasyToAddANewArtistJustStartTypingTheNameOfTheArtistYouWantToAdd": "It's easy to add a new artist, just start typing the name of the artist you want to add.", "Label": "Label", "Language": "Language", + "Large": "Large", "LastAlbum": "Last Album", "LastDuration": "Last Duration", "LastExecution": "Last Execution", @@ -604,6 +612,7 @@ "MonitorAlbum": "Monitor Album", "MonitorAlbumExistingOnlyWarning": "This is a one off adjustment of the monitored setting for each album. Use the option under Artist/Edit to control what happens for newly added albums", "MonitorArtist": "Monitor Artist", + "MonitorArtists": "Monitor Artists", "MonitorExistingAlbums": "Monitor Existing Albums", "MonitorNewAlbums": "Monitor New Albums", "MonitorNewItems": "Monitor New Albums", @@ -954,6 +963,7 @@ "SkipFreeSpaceCheckWhenImportingHelpText": "Use when Lidarr is unable to detect free space of your root folder during file import", "SkipRedownload": "Skip Redownload", "SkipRedownloadHelpText": "Prevents Lidarr from trying download alternative releases for the removed items", + "Small": "Small", "SmartReplace": "Smart Replace", "SomeResultsAreHiddenByTheAppliedFilter": "Some results are hidden by the applied filter", "SorryThatAlbumCannotBeFound": "Sorry, that album cannot be found.", @@ -1087,6 +1097,7 @@ "UpdateCheckStartupTranslocationMessage": "Cannot install update because startup folder '{0}' is in an App Translocation folder.", "UpdateCheckUINotWritableMessage": "Cannot install update because UI folder '{0}' is not writable by the user '{1}'.", "UpdateMechanismHelpText": "Use Lidarr's built-in updater or a script", + "UpdateMonitoring": "Update Monitoring", "UpdateScriptPathHelpText": "Path to a custom script that takes an extracted update package and handle the remainder of the update process", "UpdateSelected": "Update Selected", "Updates": "Updates",