diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 94ae27a0..0a710921 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -93,7 +93,7 @@ export const App = () => { useEffect(() => { const root = document.documentElement; - root.style.setProperty('--image-fit', nativeImageAspect ? 'scale-down' : 'cover'); + root.style.setProperty('--image-fit', nativeImageAspect ? 'contain' : 'cover'); }, [nativeImageAspect]); const providerValue = useMemo(() => { diff --git a/src/renderer/components/card/card-rows.tsx b/src/renderer/components/card/card-rows.tsx index 40b1fa99..50df66b4 100644 --- a/src/renderer/components/card/card-rows.tsx +++ b/src/renderer/components/card/card-rows.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import formatDuration from 'format-duration'; import { generatePath } from 'react-router'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; @@ -6,6 +7,7 @@ import { Album, AlbumArtist, Artist, Playlist, Song } from '/@/renderer/api/type import { Text } from '/@/renderer/components/text'; import { AppRoute } from '/@/renderer/router/routes'; import { CardRow } from '/@/renderer/types'; +import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format'; const Row = styled.div<{ $secondary?: boolean }>` width: 100%; @@ -69,7 +71,10 @@ export const CardRows = ({ data, rows }: CardRowsProps) => { )} onClick={(e) => e.stopPropagation()} > - {row.arrayProperty && item[row.arrayProperty]} + {row.arrayProperty && + (row.format + ? row.format(item) + : item[row.arrayProperty])} ))} @@ -88,7 +93,8 @@ export const CardRows = ({ data, rows }: CardRowsProps) => { overflow="hidden" size={index > 0 ? 'sm' : 'md'} > - {row.arrayProperty && item[row.arrayProperty]} + {row.arrayProperty && + (row.format ? row.format(item) : item[row.arrayProperty])} ))} @@ -114,7 +120,7 @@ export const CardRows = ({ data, rows }: CardRowsProps) => { )} onClick={(e) => e.stopPropagation()} > - {data && data[row.property]} + {data && (row.format ? row.format(data) : data[row.property])} ) : ( { overflow="hidden" size={index > 0 ? 'sm' : 'md'} > - {data && data[row.property]} + {data && (row.format ? row.format(data) : data[row.property])} )} @@ -151,12 +157,15 @@ export const ALBUM_CARD_ROWS: { [key: string]: CardRow } = { }, }, createdAt: { + format: (song) => formatDateAbsolute(song.createdAt), property: 'createdAt', }, duration: { + format: (album) => (album.duration === null ? null : formatDuration(album.duration)), property: 'duration', }, lastPlayedAt: { + format: (album) => formatDateRelative(album.lastPlayedAt), property: 'lastPlayedAt', }, name: { @@ -170,6 +179,7 @@ export const ALBUM_CARD_ROWS: { [key: string]: CardRow } = { property: 'playCount', }, rating: { + format: (album) => formatRating(album), property: 'userRating', }, releaseDate: { @@ -208,12 +218,15 @@ export const SONG_CARD_ROWS: { [key: string]: CardRow } = { }, }, createdAt: { + format: (song) => formatDateAbsolute(song.createdAt), property: 'createdAt', }, duration: { + format: (song) => (song.duration === null ? null : formatDuration(song.duration)), property: 'duration', }, lastPlayedAt: { + format: (song) => formatDateRelative(song.lastPlayedAt), property: 'lastPlayedAt', }, name: { @@ -227,6 +240,7 @@ export const SONG_CARD_ROWS: { [key: string]: CardRow } = { property: 'playCount', }, rating: { + format: (song) => formatRating(song), property: 'userRating', }, releaseDate: { @@ -242,12 +256,14 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow } = { property: 'albumCount', }, duration: { + format: (artist) => (artist.duration === null ? null : formatDuration(artist.duration)), property: 'duration', }, genres: { property: 'genres', }, lastPlayedAt: { + format: (artist) => formatDateRelative(artist.lastPlayedAt), property: 'lastPlayedAt', }, name: { @@ -261,6 +277,7 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow } = { property: 'playCount', }, rating: { + format: (artist) => formatRating(artist), property: 'userRating', }, songCount: { @@ -270,6 +287,8 @@ export const ALBUMARTIST_CARD_ROWS: { [key: string]: CardRow } = { export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow } = { duration: { + format: (playlist) => + playlist.duration === null ? null : formatDuration(playlist.duration), property: 'duration', }, name: { @@ -295,7 +314,4 @@ export const PLAYLIST_CARD_ROWS: { [key: string]: CardRow } = { songCount: { property: 'songCount', }, - updatedAt: { - property: 'songCount', - }, }; diff --git a/src/renderer/components/virtual-table/index.tsx b/src/renderer/components/virtual-table/index.tsx index 6428e8bc..95a1bdeb 100644 --- a/src/renderer/components/virtual-table/index.tsx +++ b/src/renderer/components/virtual-table/index.tsx @@ -15,8 +15,6 @@ import type { AgGridReactProps } from '@ag-grid-community/react'; import { AgGridReact } from '@ag-grid-community/react'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { useClickOutside, useMergedRef } from '@mantine/hooks'; -import dayjs from 'dayjs'; -import relativeTime from 'dayjs/plugin/relativeTime'; import formatDuration from 'format-duration'; import { AnimatePresence } from 'framer-motion'; import { generatePath } from 'react-router'; @@ -43,7 +41,7 @@ import { useFixedTableHeader } from '/@/renderer/components/virtual-table/hooks/ import { NoteCell } from '/@/renderer/components/virtual-table/cells/note-cell'; import { RowIndexCell } from '/@/renderer/components/virtual-table/cells/row-index-cell'; import i18n from '/@/i18n/i18n'; -import { formatSizeString } from '/@/renderer/utils/format-size-string'; +import { formatDateAbsolute, formatDateRelative, formatSizeString } from '/@/renderer/utils/format'; export * from './table-config-dropdown'; export * from './table-pagination'; @@ -64,8 +62,6 @@ const DummyHeader = styled.div<{ height?: number }>` height: ${({ height }) => height || 36}px; `; -dayjs.extend(relativeTime); - const tableColumns: { [key: string]: ColDef } = { actions: { cellClass: 'ag-cell-favorite', @@ -182,8 +178,7 @@ const tableColumns: { [key: string]: ColDef } = { GenericTableHeader(params, { position: 'center' }), headerName: i18n.t('table.column.dateAdded'), suppressSizeToFit: true, - valueFormatter: (params: ValueFormatterParams) => - params.value ? dayjs(params.value).format('MMM D, YYYY') : '', + valueFormatter: (params: ValueFormatterParams) => formatDateAbsolute(params.value), valueGetter: (params: ValueGetterParams) => params.data ? params.data.createdAt : undefined, width: 130, @@ -225,8 +220,7 @@ const tableColumns: { [key: string]: ColDef } = { headerComponent: (params: IHeaderParams) => GenericTableHeader(params, { position: 'center' }), headerName: i18n.t('table.column.lastPlayed'), - valueFormatter: (params: ValueFormatterParams) => - params.value ? dayjs(params.value).fromNow() : '', + valueFormatter: (params: ValueFormatterParams) => formatDateRelative(params.value), valueGetter: (params: ValueGetterParams) => params.data ? params.data.lastPlayedAt : undefined, width: 130, @@ -258,8 +252,7 @@ const tableColumns: { [key: string]: ColDef } = { GenericTableHeader(params, { position: 'center' }), headerName: i18n.t('table.column.releaseDate'), suppressSizeToFit: true, - valueFormatter: (params: ValueFormatterParams) => - params.value ? dayjs(params.value).format('MMM D, YYYY') : '', + valueFormatter: (params: ValueFormatterParams) => formatDateAbsolute(params.value), valueGetter: (params: ValueGetterParams) => params.data ? params.data.releaseDate : undefined, width: 130, diff --git a/src/renderer/features/item-details/components/item-details-modal.tsx b/src/renderer/features/item-details/components/item-details-modal.tsx index 5e36014a..c0553fd3 100644 --- a/src/renderer/features/item-details/components/item-details-modal.tsx +++ b/src/renderer/features/item-details/components/item-details-modal.tsx @@ -1,13 +1,11 @@ import { Group, Table } from '@mantine/core'; -import dayjs from 'dayjs'; import { RiCheckFill, RiCloseFill } from 'react-icons/ri'; import { TFunction, useTranslation } from 'react-i18next'; import { ReactNode } from 'react'; import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Song } from '/@/renderer/api/types'; -import { formatDurationString } from '/@/renderer/utils'; -import { formatSizeString } from '/@/renderer/utils/format-size-string'; +import { formatDurationString, formatSizeString } from '/@/renderer/utils'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; -import { Rating, Spoiler, Text } from '/@/renderer/components'; +import { Spoiler, Text } from '/@/renderer/components'; import { sanitize } from '/@/renderer/utils/sanitize'; import { SongPath } from '/@/renderer/features/item-details/components/song-path'; import { generatePath } from 'react-router'; @@ -15,6 +13,7 @@ import { Link } from 'react-router-dom'; import { AppRoute } from '/@/renderer/router/routes'; import { Separator } from '/@/renderer/components/separator'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route'; +import { formatDateRelative, formatRating } from '/@/renderer/utils/format'; export type ItemDetailsModalProps = { item: Album | AlbumArtist | Song; @@ -82,8 +81,6 @@ const formatArtists = (isAlbumArtist: boolean) => (item: Album | Song) => const formatComment = (item: Album | Song) => item.comment ? {replaceURLWithHTMLLinks(item.comment)} : null; -const formatDate = (key: string | null) => (key ? dayjs(key).fromNow() : ''); - const FormatGenre = (item: Album | AlbumArtist | Song) => { const genreRoute = useGenreRoute(); @@ -104,14 +101,6 @@ const FormatGenre = (item: Album | AlbumArtist | Song) => { )); }; -const formatRating = (item: Album | AlbumArtist | Song) => - item.userRating !== null ? ( - - ) : null; - const BoolField = (key: boolean) => key ? : ; @@ -139,11 +128,11 @@ const AlbumPropertyMapping: ItemDetailRow[] = [ { key: 'playCount', label: 'filter.playCount' }, { label: 'filter.lastPlayed', - render: (song) => formatDate(song.lastPlayedAt), + render: (song) => formatDateRelative(song.lastPlayedAt), }, { label: 'common.modified', - render: (song) => formatDate(song.updatedAt), + render: (song) => formatDateRelative(song.updatedAt), }, { label: 'filter.comment', render: formatComment }, { @@ -178,7 +167,7 @@ const AlbumArtistPropertyMapping: ItemDetailRow[] = [ { key: 'playCount', label: 'filter.playCount' }, { label: 'filter.lastPlayed', - render: (song) => formatDate(song.lastPlayedAt), + render: (song) => formatDateRelative(song.lastPlayedAt), }, { label: 'common.mbid', @@ -256,11 +245,11 @@ const SongPropertyMapping: ItemDetailRow[] = [ { key: 'playCount', label: 'filter.playCount' }, { label: 'filter.lastPlayed', - render: (song) => formatDate(song.lastPlayedAt), + render: (song) => formatDateRelative(song.lastPlayedAt), }, { label: 'common.modified', - render: (song) => formatDate(song.updatedAt), + render: (song) => formatDateRelative(song.updatedAt), }, { label: 'common.albumGain', diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 4d975548..f40863a4 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -1,3 +1,4 @@ +import { ReactNode } from 'react'; import { ServerFeatures } from '/@/renderer/api/features-types'; import { Album, @@ -37,6 +38,7 @@ export type TableType = export type CardRow = { arrayProperty?: string; + format?: (value: T) => ReactNode; property: keyof T; route?: CardRoute; }; diff --git a/src/renderer/utils/format-duration-string.ts b/src/renderer/utils/format-duration-string.ts deleted file mode 100644 index 8edbe958..00000000 --- a/src/renderer/utils/format-duration-string.ts +++ /dev/null @@ -1,24 +0,0 @@ -import formatDuration from 'format-duration'; - -export const formatDurationString = (duration: number) => { - const rawDuration = formatDuration(duration).split(':'); - - let string; - - switch (rawDuration.length) { - case 1: - string = `${rawDuration[0]} sec`; - break; - case 2: - string = `${rawDuration[0]} min ${rawDuration[1]} sec`; - break; - case 3: - string = `${rawDuration[0]} hr ${rawDuration[1]} min ${rawDuration[2]} sec`; - break; - case 4: - string = `${rawDuration[0]} day ${rawDuration[1]} hr ${rawDuration[2]} min ${rawDuration[3]} sec`; - break; - } - - return string; -}; diff --git a/src/renderer/utils/format-size-string.ts b/src/renderer/utils/format-size-string.ts deleted file mode 100644 index e24f300e..00000000 --- a/src/renderer/utils/format-size-string.ts +++ /dev/null @@ -1,12 +0,0 @@ -const SIZES = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; - -export const formatSizeString = (size?: number): string => { - let count = 0; - let finalSize = size ?? 0; - while (finalSize > 1024) { - finalSize /= 1024; - count += 1; - } - - return `${finalSize.toFixed(2)} ${SIZES[count]}`; -}; diff --git a/src/renderer/utils/format.tsx b/src/renderer/utils/format.tsx new file mode 100644 index 00000000..572905d2 --- /dev/null +++ b/src/renderer/utils/format.tsx @@ -0,0 +1,56 @@ +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import formatDuration from 'format-duration'; +import type { Album, AlbumArtist, Song } from '/@/renderer/api/types'; +import { Rating } from '/@/renderer/components/rating'; + +dayjs.extend(relativeTime); + +export const formatDateAbsolute = (key: string | null) => + key ? dayjs(key).format('MMM D, YYYY') : ''; + +export const formatDateRelative = (key: string | null) => (key ? dayjs(key).fromNow() : ''); + +export const formatDurationString = (duration: number) => { + const rawDuration = formatDuration(duration).split(':'); + + let string; + + switch (rawDuration.length) { + case 1: + string = `${rawDuration[0]} sec`; + break; + case 2: + string = `${rawDuration[0]} min ${rawDuration[1]} sec`; + break; + case 3: + string = `${rawDuration[0]} hr ${rawDuration[1]} min ${rawDuration[2]} sec`; + break; + case 4: + string = `${rawDuration[0]} day ${rawDuration[1]} hr ${rawDuration[2]} min ${rawDuration[3]} sec`; + break; + } + + return string; +}; + +export const formatRating = (item: Album | AlbumArtist | Song) => + item.userRating !== null ? ( + + ) : null; + +const SIZES = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; + +export const formatSizeString = (size?: number): string => { + let count = 0; + let finalSize = size ?? 0; + while (finalSize > 1024) { + finalSize /= 1024; + count += 1; + } + + return `${finalSize.toFixed(2)} ${SIZES[count]}`; +}; diff --git a/src/renderer/utils/index.ts b/src/renderer/utils/index.ts index 83770108..46e06c13 100644 --- a/src/renderer/utils/index.ts +++ b/src/renderer/utils/index.ts @@ -5,6 +5,6 @@ export * from './constrain-sidebar-width'; export * from './title-case'; export * from './get-header-color'; export * from './parse-search-params'; -export * from './format-duration-string'; export * from './rgb-to-rgba'; export * from './sentence-case'; +export * from './format';