nukeop/nuclear

View on GitHub
packages/app/app/components/PlaylistView/index.tsx

Summary

Maintainability
A
45 mins
Test Coverage
import React, { useCallback } from 'react';
import { TFunction } from 'i18next';
import { useTranslation } from 'react-i18next';
import { useHistory } from 'react-router';
import { Icon } from 'semantic-ui-react';

import { Playlist } from '@nuclear/core';
import { Button, ContextPopup, PopupButton, InputDialog, timestampToTimeString } from '@nuclear/ui';
import { Track } from '@nuclear/ui/lib/types';

import artPlaceholder from '../../../resources/media/art_placeholder.png';

import styles from './styles.scss';
import TrackTableContainer from '../../containers/TrackTableContainer';

export type PlaylistViewProps = {
  playlist: Playlist;
  updatePlaylist: (playlist: Playlist) => void;
  deletePlaylist: (id: string) => void;
  exportPlaylist: (playlist: Playlist, t: TFunction) => void;
  clearQueue: () => void;
  startPlayback: (fromMain: boolean) => void;
  selectSong: (i: number) => void;
  addTracks: (tracks: Playlist['tracks']) => void;
  onReorderTracks: (isource: number, idest: number) => void;
  isEditable?: boolean;
}

const PlaylistView: React.FC<PlaylistViewProps> = ({
  playlist,
  updatePlaylist,
  deletePlaylist,
  exportPlaylist,
  clearQueue,
  addTracks,
  onReorderTracks,
  selectSong,
  startPlayback,
  isEditable = true
}) => {
  const { t, i18n } = useTranslation('playlists');
  const history = useHistory();

  const onRenamePlaylist = useCallback((name: string) => {
    const updatedPlaylist = {
      ...playlist,
      name
    };
    updatePlaylist(updatedPlaylist);
  }, [playlist, updatePlaylist]);

  const onAddAll = useCallback(() => addTracks(playlist.tracks),
    [addTracks, playlist]);

  const onPlayAll = useCallback(() => {
    clearQueue();
    addTracks(playlist.tracks);
    selectSong(0);
    startPlayback(false);
  }, [addTracks, clearQueue, playlist, selectSong, startPlayback]);

  const onDeleteTrack = isEditable
    ? useCallback((trackToRemove: Track, trackIndex: number) => {
      const newPlaylist = {
        ...playlist,
        tracks: playlist.tracks.filter((_, index) => index !== trackIndex)
      };
      updatePlaylist(newPlaylist);
    }, [playlist, updatePlaylist])
    : undefined;

  const onDeletePlaylist = useCallback(() => {
    deletePlaylist(playlist.id);
    if (history.length > 1) {
      // The current playlist is the top of the navigation stack, go back to the previous view
      history.goBack();
    } else {
      // Fallback in case we can't navigate back to a previous view
      history.push('/playlists');
    }
  }, [playlist, history, deletePlaylist]);

  const onExportPlaylist = useCallback(() => {
    exportPlaylist(playlist, t);
  }, [exportPlaylist, playlist, t]);

  return (
    <div 
      data-testid='playlist-view'
      className={styles.playlist_view_container}
    >
      <div className={styles.playlist}>
        <div className={styles.playlist_view_info}>
          <div>
            <img
              className={styles.playlist_thumbnail}
              src={playlist?.tracks?.[0]?.thumbnail ?? artPlaceholder as unknown as string}
            />
          </div>
          <div className={styles.playlist_header}>
            <label className={styles.playlist_header_label}>{t('playlist')}</label>
            <div className={styles.playlist_name}>
              {playlist.name}
              <InputDialog
                header={t('create-playlist-dialog-title')}
                placeholder={t('dialog-placeholder')}
                acceptLabel={t('dialog-rename')}
                cancelLabel={t('dialog-cancel')}
                initialString={playlist.name}
                onAccept={onRenamePlaylist}
                trigger={
                  isEditable &&
                  <Button
                    basic
                    aria-label={t('rename')}
                    icon='pencil'
                    data-testid='rename-button'
                  />
                }
              />
            </div>
            <div className={styles.playlist_details}>
              <span>
                {`${playlist.tracks.length} ${t('number-of-tracks')}`}
              </span>
              {
                playlist.lastModified &&
                  <>
                    <span>
                      ยท
                    </span>

                    <span>
                      {`${t('modified-at')}${timestampToTimeString(playlist.lastModified, i18n.language)}`}
                    </span>
                  </>
              }
            </div>
            <div className={styles.playlist_buttons}>
              <Button
                onClick={onPlayAll}
                color='pink'
                circular
                className={styles.play_button}
              >
                <Icon name='play' /> {t('play')}
              </Button>

              <ContextPopup
                trigger={
                  <Button
                    basic
                    circular
                    data-testid='more-button'
                    className={styles.more_button}
                  >
                    <Icon name='ellipsis horizontal' />
                  </Button>
                }
                artist={null}
                title={playlist.name}
                thumb={playlist?.tracks?.[0]?.thumbnail ?? artPlaceholder as unknown as string}
              >
                <PopupButton
                  onClick={onAddAll}
                  ariaLabel={t('queue')}
                  icon='plus'
                  label={t('queue')}
                />
                {
                  isEditable &&
                  <PopupButton
                    onClick={onDeletePlaylist}
                    ariaLabel={t('delete')}
                    icon='trash'
                    label={t('delete')}
                  />
                }
                <PopupButton
                  onClick={onExportPlaylist}
                  ariaLabel={t('export-button')}
                  icon='download'
                  label={t('export-button')}
                />
              </ContextPopup>
            </div>
          </div>
        </div>
        <TrackTableContainer
          tracks={playlist.tracks as Track[]}
          onDelete={onDeleteTrack}
          onReorder={onReorderTracks}
          displayAlbum={false}
          displayDeleteButton={isEditable}
        />
      </div>
    </div>
  );
};

export default PlaylistView;