diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index d54287ca0..3b717c657 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -275,10 +275,10 @@ "explicitStatus": "$t(common.explicitStatus)" }, "datetime": { - "minuteShort": "min", - "secondShort": "sec", - "hourShort": "hr", - "dayShort": "day" + "minuteShort": "m", + "secondShort": "s", + "hourShort": "h", + "dayShort": "d" }, "filterOperator": { "after": "is after", diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index 7c7dc8917..4bc1b308a 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -1,11 +1,11 @@ import clsx from 'clsx'; -import formatDuration from 'format-duration'; import { AnimatePresence } from 'motion/react'; import { Fragment, memo, ReactNode, useState } from 'react'; import { generatePath, Link } from 'react-router'; import styles from './item-card.module.css'; +import i18n from '/@/i18n/i18n'; import { ItemCardControls } from '/@/renderer/components/item-card/item-card-controls'; import { ItemImage } from '/@/renderer/components/item-image/item-image'; import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; @@ -19,7 +19,15 @@ import { ItemControls } from '/@/renderer/components/item-list/types'; import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { AppRoute } from '/@/renderer/router/routes'; import { useGeneralSettings } from '/@/renderer/store'; -import { formatDateAbsolute, formatDateRelative, formatRating } from '/@/renderer/utils/format'; +import { + formatDateAbsolute, + formatDateAbsoluteUTC, + formatDateRelative, + formatDurationString, + formatRating, +} from '/@/renderer/utils/format'; +import { Group } from '/@/shared/components/group/group'; +import { Icon } from '/@/shared/components/icon/icon'; import { Separator } from '/@/shared/components/separator/separator'; import { Skeleton } from '/@/shared/components/skeleton/skeleton'; import { Text } from '/@/shared/components/text/text'; @@ -991,7 +999,7 @@ export const getDataRows = (): DataRow[] => { { format: (data) => { if ('duration' in data && data.duration !== null) { - return formatDuration(data.duration * 1000); + return formatDurationString(data.duration); } return ''; }, @@ -1009,7 +1017,7 @@ export const getDataRows = (): DataRow[] => { { format: (data) => { if ('releaseDate' in data && data.releaseDate) { - return data.releaseDate; + return formatDateAbsoluteUTC(data.releaseDate); } return ''; }, @@ -1027,7 +1035,12 @@ export const getDataRows = (): DataRow[] => { { format: (data) => { if ('lastPlayedAt' in data && data.lastPlayedAt) { - return formatDateRelative(data.lastPlayedAt); + return ( + + + {formatDateRelative(data.lastPlayedAt)} + + ); } return ''; }, @@ -1036,7 +1049,7 @@ export const getDataRows = (): DataRow[] => { { format: (data) => { if ('playCount' in data && data.playCount !== null) { - return String(data.playCount); + return i18n.t('entity.play', { count: data.playCount }); } return ''; }, @@ -1085,7 +1098,7 @@ export const getDataRows = (): DataRow[] => { { format: (data) => { if ('songCount' in data && data.songCount !== null) { - return String(data.songCount); + return i18n.t('entity.trackWithCount', { count: data.songCount }); } return ''; }, diff --git a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx index 32c89f467..070ea6d65 100644 --- a/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/title-combined-column.tsx @@ -74,8 +74,8 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => { }; const artists = useMemo(() => { - if (row && 'artists' in row && Array.isArray(row.artists)) { - return (row.artists as RelatedAlbumArtist[]).map((artist) => { + if (row && 'artists' in item && Array.isArray(item.artists)) { + return (item.artists as RelatedAlbumArtist[]).map((artist) => { const path = generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, { artistId: artist.id, }); @@ -83,9 +83,9 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => { }); } return []; - }, [row]); + }, [item, row]); - if (row && 'name' in row && 'imageUrl' in row && 'artists' in row) { + if (item && 'name' in item && 'imageUrl' in item && 'artists' in item) { const rowHeight = props.getRowHeight(props.rowIndex, props); const path = getTitlePath(props.itemType, (props.data[props.rowIndex] as any).id as string); @@ -143,7 +143,7 @@ export const DefaultTitleCombinedColumn = (props: ItemTableListInnerColumn) => { })} > - {row.name as string} + {item.name as string}
{artists.map((artist, index) => ( diff --git a/src/renderer/components/item-list/item-table-list/default-columns.ts b/src/renderer/components/item-list/item-table-list/default-columns.ts index 9a781d33a..e5c058719 100644 --- a/src/renderer/components/item-list/item-table-list/default-columns.ts +++ b/src/renderer/components/item-list/item-table-list/default-columns.ts @@ -18,34 +18,34 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ autoSize: false, isEnabled: true, label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.ROW_INDEX, - width: 80, + width: 60, }, { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.IMAGE, width: 70, }, { align: 'start', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.TITLE, width: 300, }, { align: 'start', autoSize: false, - isEnabled: false, + isEnabled: true, label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.TITLE_COMBINED, width: 300, }, @@ -61,7 +61,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'start', autoSize: false, - isEnabled: false, + isEnabled: true, label: i18n.t('table.config.label.album', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.ALBUM, @@ -70,7 +70,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'start', autoSize: true, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.ALBUM_ARTIST, @@ -115,7 +115,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.RELEASE_DATE, @@ -178,7 +178,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.LAST_PLAYED, @@ -214,7 +214,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.DATE_ADDED, @@ -232,7 +232,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.PLAY_COUNT, @@ -252,7 +252,7 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ autoSize: false, isEnabled: true, label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.USER_FAVORITE, width: 60, }, @@ -268,9 +268,9 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.ACTIONS, width: 60, }, @@ -284,34 +284,34 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ autoSize: false, isEnabled: true, label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.ROW_INDEX, - width: 80, + width: 60, }, { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.IMAGE, width: 70, }, { align: 'start', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.TITLE, width: 300, }, { align: 'start', autoSize: false, - isEnabled: false, + isEnabled: true, label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }), - pinned: 'left', + pinned: null, value: TableColumn.TITLE_COMBINED, width: 300, }, @@ -327,7 +327,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'start', autoSize: true, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.albumArtist', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.ALBUM_ARTIST, @@ -381,7 +381,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.releaseDate', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.RELEASE_DATE, @@ -390,7 +390,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.lastPlayed', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.LAST_PLAYED, @@ -399,7 +399,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.dateAdded', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.DATE_ADDED, @@ -408,7 +408,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.playCount', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.PLAY_COUNT, @@ -419,7 +419,7 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ autoSize: false, isEnabled: true, label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.USER_FAVORITE, width: 60, }, @@ -435,9 +435,9 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.ACTIONS, width: 60, }, @@ -451,7 +451,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [ label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.ROW_INDEX, - width: 80, + width: 60, }, { align: 'center', @@ -539,7 +539,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [ autoSize: false, isEnabled: true, label: i18n.t('table.config.label.favorite', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.USER_FAVORITE, width: 60, }, @@ -555,9 +555,9 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.ACTIONS, width: 60, }, @@ -571,7 +571,7 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [ label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.ROW_INDEX, - width: 80, + width: 60, }, { align: 'center', @@ -630,9 +630,9 @@ export const PLAYLIST_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.ACTIONS, width: 60, }, @@ -646,7 +646,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [ label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }), pinned: null, value: TableColumn.ROW_INDEX, - width: 80, + width: 60, }, { align: 'start', @@ -678,9 +678,9 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [ { align: 'center', autoSize: false, - isEnabled: true, + isEnabled: false, label: i18n.t('table.config.label.actions', { postProcess: 'titleCase' }), - pinned: 'right', + pinned: null, value: TableColumn.ACTIONS, width: 60, }, @@ -728,6 +728,15 @@ export const pickTableColumns = (options: { const enabledSet = new Set(enabledColumns); const remaining = columns.filter((col) => !enabledSet.has(col.value)); columnsToProcess = [...columnsToProcess, ...remaining]; + } else { + // When pickColumns is provided, include pickColumns that aren't in enabledColumns + // so they can be added as disabled entries + const enabledSet = new Set(enabledColumns); + const pickColumnsNotEnabled = pickColumns + .filter((col) => !enabledSet.has(col)) + .map((col) => columnMap.get(col)) + .filter((col): col is DefaultTableColumn => col !== undefined); + columnsToProcess = [...columnsToProcess, ...pickColumnsNotEnabled]; } } else { columnsToProcess = columns; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 45bcbf916..cbda16253 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -969,13 +969,11 @@ const initialState: SettingsState = { [TableColumn.TITLE]: 400, [TableColumn.TRACK_NUMBER]: 50, [TableColumn.USER_FAVORITE]: 60, - [TableColumn.USER_RATING]: 100, }, enabledColumns: [ TableColumn.TRACK_NUMBER, TableColumn.TITLE, TableColumn.DURATION, - TableColumn.USER_RATING, TableColumn.USER_FAVORITE, ], }), @@ -1035,10 +1033,11 @@ const initialState: SettingsState = { TableColumn.BIT_RATE, TableColumn.BPM, TableColumn.DATE_ADDED, - TableColumn.DURATION, TableColumn.GENRE, TableColumn.PLAY_COUNT, TableColumn.SONG_COUNT, + TableColumn.RELEASE_DATE, + TableColumn.LAST_PLAYED, TableColumn.YEAR, ], }), @@ -1047,7 +1046,7 @@ const initialState: SettingsState = { itemsPerPage: 100, pagination: ListPaginationType.INFINITE, table: { - autoFitColumns: false, + autoFitColumns: true, columns: ALBUM_TABLE_COLUMNS.map((column) => ({ align: column.align, autoSize: column.autoSize, @@ -1085,7 +1084,7 @@ const initialState: SettingsState = { itemsPerPage: 100, pagination: ListPaginationType.INFINITE, table: { - autoFitColumns: false, + autoFitColumns: true, columns: pickTableColumns({ autoSizeColumns: [TableColumn.TITLE], columns: ALBUM_ARTIST_TABLE_COLUMNS, @@ -1093,12 +1092,7 @@ const initialState: SettingsState = { TableColumn.ROW_INDEX, TableColumn.IMAGE, TableColumn.TITLE, - TableColumn.ALBUM_COUNT, - TableColumn.SONG_COUNT, - TableColumn.PLAY_COUNT, - TableColumn.LAST_PLAYED, TableColumn.USER_FAVORITE, - TableColumn.USER_RATING, ], }), enableAlternateRowColors: false, diff --git a/src/renderer/utils/format.tsx b/src/renderer/utils/format.tsx index 2f00b4ee0..c449f85a9 100644 --- a/src/renderer/utils/format.tsx +++ b/src/renderer/utils/format.tsx @@ -99,20 +99,25 @@ export const formatDateRelative = (key: null | string) => (key ? dayjs(key).from export const formatDurationString = (duration: number) => { const rawDuration = formatDuration(duration, { leading: false }).split(':'); - let string; + const formattedDuration = rawDuration.map((part) => { + // Remove leading zero + return part.replace(/^0/, ''); + }); + + let string: string = ''; switch (rawDuration.length) { case 1: - string = `${rawDuration[0]} ${i18n.t('datetime.secondShort')}`; + string = `${formattedDuration[0]}${i18n.t('datetime.secondShort')}`; break; case 2: - string = `${rawDuration[0]} ${i18n.t('datetime.minuteShort')} ${rawDuration[1]} ${i18n.t('datetime.secondShort')}`; + string = `${formattedDuration[0]}${i18n.t('datetime.minuteShort')} ${formattedDuration[1]}${i18n.t('datetime.secondShort')}`; break; case 3: - string = `${rawDuration[0]} ${i18n.t('datetime.hourShort')} ${rawDuration[1]} ${i18n.t('datetime.minuteShort')} ${rawDuration[2]} ${i18n.t('datetime.secondShort')}`; + string = `${formattedDuration[0]}${i18n.t('datetime.hourShort')} ${formattedDuration[1]}${i18n.t('datetime.minuteShort')} ${formattedDuration[2]}${i18n.t('datetime.secondShort')}`; break; case 4: - string = `${rawDuration[0]} ${i18n.t('datetime.dayShort')} ${rawDuration[1]} ${i18n.t('datetime.hourShort')} ${rawDuration[2]} ${i18n.t('datetime.minuteShort')} ${rawDuration[3]} ${i18n.t('datetime.secondShort')}`; + string = `${formattedDuration[0]}${i18n.t('datetime.dayShort')} ${formattedDuration[1]}${i18n.t('datetime.hourShort')} ${formattedDuration[2]}${i18n.t('datetime.minuteShort')} ${formattedDuration[3]}${i18n.t('datetime.secondShort')}`; break; } diff --git a/src/shared/components/icon/icon.tsx b/src/shared/components/icon/icon.tsx index 969ce13bb..d17cce019 100644 --- a/src/shared/components/icon/icon.tsx +++ b/src/shared/components/icon/icon.tsx @@ -49,6 +49,7 @@ import { LuGripVertical, LuHardDrive, LuHash, + LuHeadphones, LuHeart, LuHeartCrack, LuImage, @@ -180,6 +181,7 @@ export const AppIcon = { itemAlbum: LuDisc3, itemSong: LuMusic, keyboard: LuKeyboard, + lastPlayed: LuHeadphones, layoutGrid: LuLayoutGrid, layoutList: LuList, layoutTable: LuTable,