import { ReactNode } from 'react'; import { TFunction, useTranslation } from 'react-i18next'; import { generatePath } from 'react-router'; import { Link } from 'react-router-dom'; import { SongPath } from '/@/renderer/features/item-details/components/song-path'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; import { AppRoute } from '/@/renderer/router/routes'; import { formatDurationString, formatSizeString } from '/@/renderer/utils'; import { formatDateRelative, formatRating } from '/@/renderer/utils/format'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; import { sanitize } from '/@/renderer/utils/sanitize'; import { SEPARATOR_STRING } from '/@/shared/api/utils'; import { Icon } from '/@/shared/components/icon/icon'; import { Separator } from '/@/shared/components/separator/separator'; import { Spoiler } from '/@/shared/components/spoiler/spoiler'; import { Table } from '/@/shared/components/table/table'; import { Text } from '/@/shared/components/text/text'; import { ExplicitStatus } from '/@/shared/types/domain-types'; import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Playlist, RelatedArtist, Song, } from '/@/shared/types/domain-types'; export type ItemDetailsModalProps = { item: Album | AlbumArtist | Playlist | Song; }; type ItemDetailRow = { key?: keyof T; label: string; postprocess?: string[]; render?: (item: T, t: TFunction) => ReactNode; }; const handleRow = (t: TFunction, item: T, rule: ItemDetailRow) => { let value: ReactNode; if (rule.render) { value = rule.render(item, t); } else { const prop = item[rule.key!]; value = prop !== undefined && prop !== null ? String(prop) : null; } if (!value) return null; return ( {t(rule.label, { postProcess: rule.postprocess || 'sentenceCase' })} {value} ); }; const formatArtists = (artists: null | RelatedArtist[] | undefined) => artists?.map((artist, index) => ( {index > 0 && } {artist.id ? ( {artist.name || '—'} ) : ( {artist.name || '-'} )} )); const formatComment = (item: Album | Song) => item.comment ? {replaceURLWithHTMLLinks(item.comment)} : null; const FormatGenre = (item: Album | AlbumArtist | Playlist | Song) => { const genreRoute = useGenreRoute(); if (!item.genres?.length) { return null; } return item.genres?.map((genre, index) => ( {index > 0 && } {genre.name || '—'} )); }; const BoolField = (key: boolean) => key ? : ; const AlbumPropertyMapping: ItemDetailRow[] = [ { key: 'name', label: 'common.title' }, { label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) }, { label: 'entity.genre_other', render: FormatGenre }, { label: 'common.duration', render: (album) => album.duration && formatDurationString(album.duration), }, { key: 'releaseYear', label: 'filter.releaseYear' }, { key: 'songCount', label: 'filter.songCount' }, { label: 'filter.explicitStatus', render: (album, t) => album.explicitStatus === ExplicitStatus.EXPLICIT ? t('common.explicit', { postProcess: 'sentenceCase' }) : album.explicitStatus === ExplicitStatus.CLEAN ? t('common.clean', { postProcess: 'sentenceCase' }) : null, }, { label: 'filter.isCompilation', render: (album) => BoolField(album.isCompilation || false) }, { key: 'size', label: 'common.size', render: (album) => album.size && formatSizeString(album.size), }, { label: 'common.favorite', render: (album) => BoolField(album.userFavorite), }, { label: 'common.rating', render: formatRating }, { key: 'playCount', label: 'filter.playCount' }, { label: 'filter.lastPlayed', render: (song) => formatDateRelative(song.lastPlayedAt), }, { label: 'common.modified', render: (song) => formatDateRelative(song.updatedAt), }, { label: 'filter.comment', render: formatComment }, { label: 'common.mbid', postprocess: [], render: (album) => album.mbzId ? ( {album.mbzId} ) : null, }, { key: 'id', label: 'filter.id' }, ]; const AlbumArtistPropertyMapping: ItemDetailRow[] = [ { key: 'name', label: 'common.name' }, { label: 'entity.genre_other', render: FormatGenre }, { label: 'common.duration', render: (artist) => artist.duration && formatDurationString(artist.duration), }, { key: 'songCount', label: 'filter.songCount' }, { label: 'common.favorite', render: (artist) => BoolField(artist.userFavorite), }, { label: 'common.rating', render: formatRating }, { key: 'playCount', label: 'filter.playCount' }, { label: 'filter.lastPlayed', render: (song) => formatDateRelative(song.lastPlayedAt), }, { label: 'common.mbid', postprocess: [], render: (artist) => artist.mbz ? ( {artist.mbz} ) : null, }, { label: 'common.biography', render: (artist) => artist.biography ? ( ) : null, }, { key: 'id', label: 'filter.id' }, ]; const PlaylistPropertyMapping: ItemDetailRow[] = [ { key: 'name', label: 'common.title' }, { key: 'description', label: 'common.description' }, { label: 'entity.genre_other', render: FormatGenre }, { label: 'common.duration', render: (playlist) => playlist.duration && formatDurationString(playlist.duration), }, { key: 'songCount', label: 'filter.songCount' }, { key: 'size', label: 'common.size', render: (playlist) => playlist.size && formatSizeString(playlist.size), }, { key: 'owner', label: 'common.owner' }, { key: 'public', label: 'form.createPlaylist.input_public' }, { label: 'entity.smartPlaylist', render: (playlist) => (playlist.rules ? BoolField(true) : null), }, { key: 'id', label: 'filter.id' }, ]; const SongPropertyMapping: ItemDetailRow[] = [ { key: 'name', label: 'common.title' }, { key: 'path', label: 'common.path', render: SongPath }, { label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) }, { key: 'artists', label: 'entity.artist_other', render: (item) => formatArtists(item.artists) }, { key: 'album', label: 'entity.album_one', render: (song) => song.albumId && song.album && ( {song.album} ), }, { key: 'discNumber', label: 'common.disc' }, { key: 'trackNumber', label: 'common.trackNumber' }, { key: 'releaseYear', label: 'filter.releaseYear' }, { label: 'filter.explicitStatus', render: (song, t) => song.explicitStatus === ExplicitStatus.EXPLICIT ? t('common.explicit', { postProcess: 'sentenceCase' }) : song.explicitStatus === ExplicitStatus.CLEAN ? t('common.clean', { postProcess: 'sentenceCase' }) : null, }, { label: 'entity.genre_other', render: FormatGenre }, { label: 'common.duration', render: (song) => formatDurationString(song.duration), }, { label: 'filter.isCompilation', render: (song) => BoolField(song.compilation || false) }, { key: 'container', label: 'common.codec' }, { key: 'bitRate', label: 'common.bitrate', render: (song) => `${song.bitRate} kbps` }, { key: 'sampleRate', label: 'common.sampleRate' }, { key: 'bitDepth', label: 'common.bitDepth' }, { key: 'channels', label: 'common.channel_other' }, { key: 'size', label: 'common.size', render: (song) => formatSizeString(song.size) }, { label: 'common.favorite', render: (song) => BoolField(song.userFavorite), }, { label: 'common.rating', render: formatRating }, { key: 'playCount', label: 'filter.playCount' }, { label: 'filter.lastPlayed', render: (song) => formatDateRelative(song.lastPlayedAt), }, { label: 'common.modified', render: (song) => formatDateRelative(song.updatedAt), }, { label: 'common.albumGain', render: (song) => (song.gain?.album !== undefined ? `${song.gain.album} dB` : null), }, { label: 'common.trackGain', render: (song) => (song.gain?.track !== undefined ? `${song.gain.track} dB` : null), }, { label: 'common.albumPeak', render: (song) => (song.peak?.album !== undefined ? `${song.peak.album}` : null), }, { label: 'common.trackPeak', render: (song) => (song.peak?.track !== undefined ? `${song.peak.track}` : null), }, { label: 'filter.comment', render: formatComment }, { key: 'id', label: 'filter.id' }, ]; const handleTags = (item: Album | Song, t: TFunction) => { if (item.tags) { const tags = Object.entries(item.tags).map(([tag, fields]) => { return ( {tag.slice(0, 1).toLocaleUpperCase()} {tag.slice(1)} {fields.length === 0 ? BoolField(true) : fields.join(SEPARATOR_STRING)} ); }); if (tags.length) { return [ {t('common.tags', { postProcess: 'sentenceCase' })} {tags.length} , ].concat(tags); } } return []; }; const handleParticipants = (item: Album | Song, t: TFunction) => { if (item.participants) { const participants = Object.entries(item.participants).map(([role, participants]) => { return ( {role.slice(0, 1).toLocaleUpperCase()} {role.slice(1)} {formatArtists(participants)} ); }); if (participants.length) { return [ {t('common.additionalParticipants', { postProcess: 'sentenceCase', })} {participants.length} , ].concat(participants); } } return []; }; export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => { const { t } = useTranslation(); let body: ReactNode[] = []; switch (item.itemType) { case LibraryItem.ALBUM: body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule)); body.push(...handleParticipants(item, t)); body.push(...handleTags(item, t)); break; case LibraryItem.ALBUM_ARTIST: body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, item, rule)); break; case LibraryItem.PLAYLIST: body = PlaylistPropertyMapping.map((rule) => handleRow(t, item, rule)); break; case LibraryItem.SONG: body = SongPropertyMapping.map((rule) => handleRow(t, item, rule)); body.push(...handleParticipants(item, t)); body.push(...handleTags(item, t)); break; default: body = []; } return ( {body}
); };