diff --git a/packages/app/app/components/AlbumView/index.tsx b/packages/app/app/components/AlbumView/index.tsx index 20ef83fea7..39d51e787e 100644 --- a/packages/app/app/components/AlbumView/index.tsx +++ b/packages/app/app/components/AlbumView/index.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { useState } from 'react'; import Img from 'react-image'; import _ from 'lodash'; -import { Dimmer, Icon, Loader } from 'semantic-ui-react'; +import { Dimmer, Icon, Loader, Dropdown } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; -import { Loader as NuclearLoader, ContextPopup, PopupButton } from '@nuclear/ui'; +import { Loader as NuclearLoader, ContextPopup, PopupButton, PopupDropdown, InputDialog } from '@nuclear/ui'; import styles from './styles.scss'; import artPlaceholder from '../../../resources/media/art_placeholder.png'; import TrackTableContainer from '../../containers/TrackTableContainer'; @@ -18,6 +18,9 @@ type AlbumViewProps = { playAll: React.MouseEventHandler; removeFavoriteAlbum: React.MouseEventHandler; addFavoriteAlbum: React.MouseEventHandler; + addAlbumToPlaylist: (playlistName: string) => void; + playlistNames: string[]; + addAlbumToNewPlaylist?: (playlistName: string) => void; } export const AlbumView: React.FC = ({ @@ -28,9 +31,16 @@ export const AlbumView: React.FC = ({ addAlbumToQueue, playAll, removeFavoriteAlbum, - addFavoriteAlbum + addFavoriteAlbum, + addAlbumToPlaylist, + playlistNames, + addAlbumToNewPlaylist }) => { const { t } = useTranslation('album'); + const [isCreatePlaylistDialogOpen, setIsCreatePlaylistDialogOpen] = useState(false); + const displayPlaylistCreationDialog = () => setIsCreatePlaylistDialogOpen(true); + const hidePlaylistCreationDialog = () => setIsCreatePlaylistDialogOpen(false); + const release_date: Date = new Date(album.year); return
@@ -124,7 +134,43 @@ export const AlbumView: React.FC = ({ icon='download' label={t('download')} /> + + { + playlistNames?.map((playlistName, i) => ( + addAlbumToPlaylist(playlistName)} + > + + {playlistName} + + )) + } + displayPlaylistCreationDialog()} + data-testid='playlist-popup-create-playlist' + > + + {t('create-playlist')} + + + hidePlaylistCreationDialog()} + header={

{t('create-playlist-dialog-title')}

} + placeholder={t('create-playlist-dialog-placeholder')} + acceptLabel={t('create-playlist-dialog-accept')} + cancelLabel={t('create-playlist-dialog-cancel')} + onAccept={(input) => { + addAlbumToNewPlaylist(input); + }} + initialString={`${album.artist} - ${album.title}`} + testIdPrefix='create-playlist-dialog' + />
diff --git a/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx b/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx index a8acfbb324..b7b501cc7b 100644 --- a/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx +++ b/packages/app/app/containers/AlbumViewContainer/AlbumViewContainer.test.tsx @@ -2,6 +2,7 @@ import { waitFor } from '@testing-library/react'; import { mountedComponentFactory, setupI18Next } from '../../../test/testUtils'; import { buildStoreState } from '../../../test/storeBuilders'; import PlayerBarContainer from '../PlayerBarContainer'; +import userEvent from '@testing-library/user-event'; describe('Album view container', () => { beforeAll(() => { @@ -256,6 +257,53 @@ describe('Album view container', () => { ]); }); + it('should add album tracks to an existing playlist using the album menu popup', async () => { + const { component, store } = mountComponent( + buildStoreState() + .withAlbumDetails() + .withPlaylists([{ + id: 'test playlist id', + name: 'test playlist', + tracks: [] + }]) + .build() + ); + userEvent.click(component.getByTestId('more-button')); + userEvent.click(component.getByTestId('add-album-to-playlist')); + userEvent.click(component.getByText('test playlist')); + + const state = store.getState(); + expect(state.playlists.localPlaylists.data[0].tracks).toEqual([ + expect.objectContaining({ uuid: 'track-1-id' }), + expect.objectContaining({ uuid: 'track-2-id' }), + expect.objectContaining({ uuid: 'track-3-id' }) + ]); + }); + + it('should add album tracks to a new playlist using the album menu popup', async () => { + const { component, store } = mountComponent(); + userEvent.click(component.getByTestId('more-button')); + userEvent.click(component.getByTestId('add-album-to-playlist')); + userEvent.click(component.getByTestId('playlist-popup-create-playlist')); + + expect(component.getByTestId('create-playlist-dialog-input')).toBeVisible(); + expect(component.getByTestId('create-playlist-dialog-accept')).toBeVisible(); + expect(component.getByTestId('create-playlist-dialog-cancel')).toBeVisible(); + + userEvent.click(component.getByTestId('create-playlist-dialog-accept')); + + const state = store.getState(); + expect(state.playlists.localPlaylists.data).toHaveLength(1); + + const firstPlaylist = state.playlists.localPlaylists.data[0]; + expect(firstPlaylist.name).toBe('test artist - test album'); + expect(firstPlaylist.tracks).toEqual([ + expect.objectContaining({ uuid: 'track-1-id' }), + expect.objectContaining({ uuid: 'track-2-id' }), + expect.objectContaining({ uuid: 'track-3-id' }) + ]); + }); + const mountComponent = mountedComponentFactory( ['/album/test-album-id'], buildStoreState() diff --git a/packages/app/app/containers/AlbumViewContainer/hooks.ts b/packages/app/app/containers/AlbumViewContainer/hooks.ts index b6eec5507f..583dde7446 100644 --- a/packages/app/app/containers/AlbumViewContainer/hooks.ts +++ b/packages/app/app/containers/AlbumViewContainer/hooks.ts @@ -14,6 +14,9 @@ import { pluginsSelectors } from '../../selectors/plugins'; import { searchSelectors } from '../../selectors/search'; import { stringDurationToSeconds } from '../../utils'; import { AlbumDetailsState } from '../../reducers/search'; +import { playlistsSelectors } from '../../selectors/playlists'; +import * as PlaylistActions from '../../actions/playlists'; +import { PlaylistTrack, Track } from '@nuclear/core'; export const useAlbumViewProps = () => { const dispatch = useDispatch(); @@ -24,6 +27,7 @@ export const useAlbumViewProps = () => { // TODO replace this any with a proper type const plugins: any = useSelector(pluginsSelectors.plugins); const favoriteAlbums = useSelector(favoritesSelectors.albums); + const localPlaylists = useSelector(playlistsSelectors.localPlaylists); const albumFromFavorites = favoriteAlbums.find(album => album.id === albumId); const album = albumFromFavorites || albumDetails[albumId]; @@ -89,6 +93,32 @@ export const useAlbumViewProps = () => { dispatch(FavoritesActions.removeFavoriteAlbum(album)); }, [album, dispatch]); + const playlistNames = localPlaylists.data?.map(playlist => playlist.name); + + function getPlaylistTracks() { + const tracksWithId: PlaylistTrack[] = []; + album.tracklist?.forEach((track: Track) => tracksWithId.push(safeAddUuid(track))); + return tracksWithId; + } + + const addAlbumToPlaylist = useCallback(async (playlistName: string) => { + const tracksWithId: PlaylistTrack[] = getPlaylistTracks(); + const originalPlaylist = localPlaylists.data?.find(playlist => playlist.name === playlistName); + const playlistWithAlbumTracks = { + ...originalPlaylist, + tracks: [ + ...originalPlaylist.tracks, + ...tracksWithId + ] + }; + dispatch(PlaylistActions.updatePlaylist(playlistWithAlbumTracks)); + }, [album, localPlaylists, dispatch]); + + const addAlbumToNewPlaylist = useCallback(async (playlistName: string) => { + dispatch(PlaylistActions.addPlaylist(getPlaylistTracks(), playlistName)); + }, [album, dispatch]); + + return { album, isFavorite, @@ -97,6 +127,9 @@ export const useAlbumViewProps = () => { addAlbumToQueue, playAll, addFavoriteAlbum, - removeFavoriteAlbum + removeFavoriteAlbum, + addAlbumToPlaylist, + playlistNames, + addAlbumToNewPlaylist }; }; diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index 7d3af58f67..3427941c0d 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -8,7 +8,13 @@ "play": "Play", "queue": "Add album to queue", "tracks": "Tracks:", - "year": "Year:" + "year": "Year:", + "add-to-playlist": "Add album to playlist", + "create-playlist": "Create new playlist", + "create-playlist-dialog-title": "Input playlist name:", + "create-playlist-dialog-placeholder": "Playlist name...", + "create-playlist-dialog-accept": "Save", + "create-playlist-dialog-cancel": "Cancel" }, "app": { "collection": "Collection", @@ -462,4 +468,4 @@ "sign-in-button": "Sign in" } } -} \ No newline at end of file +}