diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index b1398f858..9374d3680 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -268,6 +268,7 @@ "comment": "Comment", "communityRating": "Community rating", "criticRating": "Critic rating", + "date": "Date", "dateAdded": "Date added", "disc": "Disc", "duration": "Duration", @@ -300,6 +301,7 @@ "title": "Title", "toYear": "To year", "trackNumber": "Track", + "year": "Year", "explicitStatus": "$t(common.explicitStatus)" }, "datetime": { @@ -1293,6 +1295,7 @@ "comment": "Comment", "dateAdded": "Date Added", "discNumber": "Disc", + "date": "Date", "favorite": "Favorite", "genre": "Genre", "lastPlayed": "Last played", @@ -1300,13 +1303,14 @@ "playCount": "Plays", "rating": "Rating", "releaseDate": "Release Date", - "releaseYear": "Year", + "releaseYear": "Release year", "sampleRate": "Sample Rate", "size": "Size", "songCount": "Tracks", "title": "Title", "trackNumber": "Track", - "owner": "Owner" + "owner": "Owner", + "year": "Year" }, "config": { "general": { @@ -1360,6 +1364,7 @@ "channels": "$t(common.channel, {\"count\": 2})", "codec": "$t(common.codec)", "composer": "Composer", + "date": "Date", "dateAdded": "Date Added", "discNumber": "Disc Number", "duration": "$t(common.duration)", @@ -1374,6 +1379,7 @@ "playCount": "Play Count", "rating": "$t(common.rating)", "releaseDate": "Release Date", + "releaseYear": "Release Year", "rowIndex": "Row Index", "sampleRate": "$t(common.sampleRate)", "size": "$t(common.size)", diff --git a/src/renderer/components/item-card/item-card.tsx b/src/renderer/components/item-card/item-card.tsx index 815e731ba..b1357bf45 100644 --- a/src/renderer/components/item-card/item-card.tsx +++ b/src/renderer/components/item-card/item-card.tsx @@ -1249,6 +1249,24 @@ export const getDataRows = (type?: 'compact' | 'default' | 'poster'): DataRow[] }, id: 'releaseDate', }, + { + format: (data) => { + if ('year' in data && data.year) { + return String(data.year); + } + return ''; + }, + id: 'year', + }, + { + format: (data) => { + if ('date' in data && data.date) { + return formatPartialIsoDateUTC(data.date); + } + return ''; + }, + id: 'date', + }, { format: (data) => { if ('createdAt' in data && data.createdAt) { diff --git a/src/renderer/components/item-list/helpers/use-grid-rows.ts b/src/renderer/components/item-list/helpers/use-grid-rows.ts index da015ad95..84c501db1 100644 --- a/src/renderer/components/item-list/helpers/use-grid-rows.ts +++ b/src/renderer/components/item-list/helpers/use-grid-rows.ts @@ -59,6 +59,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => { [TableColumn.CODEC]: null, [TableColumn.COMMENT]: null, [TableColumn.COMPOSER]: null, + [TableColumn.DATE]: 'date', [TableColumn.DATE_ADDED]: 'createdAt', [TableColumn.DISC_NUMBER]: null, [TableColumn.DURATION]: 'duration', @@ -73,6 +74,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => { [TableColumn.PLAY_COUNT]: 'playCount', [TableColumn.PLAYLIST_REORDER]: null, [TableColumn.RELEASE_DATE]: 'releaseDate', + [TableColumn.RELEASE_YEAR]: 'releaseYear', [TableColumn.ROW_INDEX]: null, [TableColumn.SAMPLE_RATE]: 'sampleRate', [TableColumn.SIZE]: null, @@ -84,7 +86,7 @@ const getRowIdFromTableColumn = (tableColumn: TableColumn): null | string => { [TableColumn.TRACK_NUMBER]: null, [TableColumn.USER_FAVORITE]: 'userFavorite', [TableColumn.USER_RATING]: 'rating', - [TableColumn.YEAR]: 'releaseYear', + [TableColumn.YEAR]: 'year', }; return columnToRowIdMap[tableColumn] || null; }; diff --git a/src/renderer/components/item-list/item-detail-list/columns/date-column.tsx b/src/renderer/components/item-list/item-detail-list/columns/date-column.tsx new file mode 100644 index 000000000..8a886a5b5 --- /dev/null +++ b/src/renderer/components/item-list/item-detail-list/columns/date-column.tsx @@ -0,0 +1,6 @@ +import { ItemDetailListCellProps } from './types'; + +import { formatPartialIsoDateUTC } from '/@/renderer/utils/format'; + +export const DateColumn = ({ song }: ItemDetailListCellProps) => + song.date ? formatPartialIsoDateUTC(song.date) : <> ; diff --git a/src/renderer/components/item-list/item-detail-list/columns/index.ts b/src/renderer/components/item-list/item-detail-list/columns/index.ts index e95b4f613..794cb1b82 100644 --- a/src/renderer/components/item-list/item-detail-list/columns/index.ts +++ b/src/renderer/components/item-list/item-detail-list/columns/index.ts @@ -14,6 +14,7 @@ import { CodecColumn } from './codec-column'; import { CommentColumn } from './comment-column'; import { ComposerColumn } from './composer-column'; import { DateAddedColumn } from './date-added-column'; +import { DateColumn } from './date-column'; import { DefaultColumn } from './default-column'; import { DiscNumberColumn } from './disc-number-column'; import { DurationColumn } from './duration-column'; @@ -26,6 +27,7 @@ import { PathColumn } from './path-column'; import { PlayCountColumn } from './play-count-column'; import { RatingColumn } from './rating-column'; import { ReleaseDateColumn } from './release-date-column'; +import { ReleaseYearColumn } from './release-year-column'; import { RowIndexColumn } from './row-index-column'; import { ItemDetailRowPlayControlCell } from './row-play-control-cell'; import { SampleRateColumn } from './sample-rate-column'; @@ -52,6 +54,7 @@ const COLUMN_MAP: Partial> = { [TableColumn.CODEC]: CodecColumn, [TableColumn.COMMENT]: CommentColumn, [TableColumn.COMPOSER]: ComposerColumn, + [TableColumn.DATE]: DateColumn, [TableColumn.DATE_ADDED]: DateAddedColumn, [TableColumn.DISC_NUMBER]: DiscNumberColumn, [TableColumn.DURATION]: DurationColumn, @@ -62,6 +65,7 @@ const COLUMN_MAP: Partial> = { [TableColumn.PATH]: PathColumn, [TableColumn.PLAY_COUNT]: PlayCountColumn, [TableColumn.RELEASE_DATE]: ReleaseDateColumn, + [TableColumn.RELEASE_YEAR]: ReleaseYearColumn, [TableColumn.ROW_INDEX]: RowIndexColumn, [TableColumn.SAMPLE_RATE]: SampleRateColumn, [TableColumn.SIZE]: SizeColumn, @@ -105,6 +109,7 @@ export { CommentColumn, ComposerColumn, DateAddedColumn, + DateColumn, DefaultColumn, DiscNumberColumn, DurationColumn, @@ -118,6 +123,7 @@ export { PlayCountColumn, RatingColumn, ReleaseDateColumn, + ReleaseYearColumn, RowIndexColumn, SampleRateColumn, SizeColumn, diff --git a/src/renderer/components/item-list/item-detail-list/columns/release-year-column.tsx b/src/renderer/components/item-list/item-detail-list/columns/release-year-column.tsx new file mode 100644 index 000000000..077a3f5fe --- /dev/null +++ b/src/renderer/components/item-list/item-detail-list/columns/release-year-column.tsx @@ -0,0 +1,4 @@ +import { ItemDetailListCellProps } from './types'; + +export const ReleaseYearColumn = ({ song }: ItemDetailListCellProps) => + song.releaseYear ? String(song.releaseYear) : <> ; diff --git a/src/renderer/components/item-list/item-detail-list/columns/year-column.tsx b/src/renderer/components/item-list/item-detail-list/columns/year-column.tsx index 9cb90c173..8a1edc9f2 100644 --- a/src/renderer/components/item-list/item-detail-list/columns/year-column.tsx +++ b/src/renderer/components/item-list/item-detail-list/columns/year-column.tsx @@ -1,4 +1,4 @@ import { ItemDetailListCellProps } from './types'; export const YearColumn = ({ song }: ItemDetailListCellProps) => - song.releaseYear ? String(song.releaseYear) : <> ; + song.year ? String(song.year) : <> ; diff --git a/src/renderer/components/item-list/item-detail-list/utils.ts b/src/renderer/components/item-list/item-detail-list/utils.ts index 6b3d13ba2..277e79bb2 100644 --- a/src/renderer/components/item-list/item-detail-list/utils.ts +++ b/src/renderer/components/item-list/item-detail-list/utils.ts @@ -7,10 +7,12 @@ const FIXED_TRACK_COLUMN_WIDTHS: Partial> = { [TableColumn.BPM]: 56, [TableColumn.CHANNELS]: 80, [TableColumn.CODEC]: 80, + [TableColumn.DATE]: 128, [TableColumn.DATE_ADDED]: 128, [TableColumn.DISC_NUMBER]: 36, [TableColumn.DURATION]: 72, [TableColumn.RELEASE_DATE]: 128, + [TableColumn.RELEASE_YEAR]: 128, [TableColumn.SAMPLE_RATE]: 112, [TableColumn.TRACK_NUMBER]: 64, [TableColumn.USER_FAVORITE]: 32, diff --git a/src/renderer/components/item-list/item-table-list/columns/release-year-column.tsx b/src/renderer/components/item-list/item-table-list/columns/release-year-column.tsx new file mode 100644 index 000000000..49f0d8be4 --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/release-year-column.tsx @@ -0,0 +1,45 @@ +import { useMemo } from 'react'; + +import { + ColumnNullFallback, + ColumnSkeletonFixed, + ItemTableListInnerColumn, + TableColumnTextContainer, +} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { SEPARATOR_STRING } from '/@/shared/api/utils'; + +const ReleaseYearColumnBase = (props: ItemTableListInnerColumn) => { + const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; + const item = rowItem as any; + + const yearDisplay = useMemo(() => { + if (item && 'releaseYear' in item && item.releaseYear != null) { + const releaseYear = item.releaseYear; + const originalYear = + 'originalYear' in item && item.originalYear > 0 ? item.originalYear : null; + + if (originalYear !== null && originalYear !== releaseYear) { + return `${originalYear}${SEPARATOR_STRING}${releaseYear}`; + } + + if (typeof releaseYear === 'number') { + return releaseYear; + } + } + return null; + }, [item]); + + if (yearDisplay !== null) { + return {yearDisplay}; + } + + const row: number | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; + + if (row === null) { + return ; + } + + return ; +}; + +export const ReleaseYearColumn = ReleaseYearColumnBase; diff --git a/src/renderer/components/item-list/item-table-list/columns/track-date-column.tsx b/src/renderer/components/item-list/item-table-list/columns/track-date-column.tsx new file mode 100644 index 000000000..fb2601f4a --- /dev/null +++ b/src/renderer/components/item-list/item-table-list/columns/track-date-column.tsx @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; + +import { + ColumnNullFallback, + ColumnSkeletonFixed, + ItemTableListInnerColumn, + TableColumnTextContainer, +} from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; +import { formatPartialIsoDateUTC } from '/@/renderer/utils/format'; + +const TrackDateColumnBase = (props: ItemTableListInnerColumn) => { + const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; + const row: string | undefined = (rowItem as any)?.[props.columns[props.columnIndex].id]; + + const dateDisplay = useMemo( + () => (typeof row === 'string' && row ? formatPartialIsoDateUTC(row) : null), + [row], + ); + + if (dateDisplay !== null) { + return {dateDisplay}; + } + + if (row === null) { + return ; + } + + return ; +}; + +export const TrackDateColumn = TrackDateColumnBase; diff --git a/src/renderer/components/item-list/item-table-list/columns/year-column.tsx b/src/renderer/components/item-list/item-table-list/columns/year-column.tsx index 7bb2ec7e5..1079e8f9a 100644 --- a/src/renderer/components/item-list/item-table-list/columns/year-column.tsx +++ b/src/renderer/components/item-list/item-table-list/columns/year-column.tsx @@ -6,24 +6,16 @@ import { ItemTableListInnerColumn, TableColumnTextContainer, } from '/@/renderer/components/item-list/item-table-list/item-table-list-column'; -import { SEPARATOR_STRING } from '/@/shared/api/utils'; const YearColumnBase = (props: ItemTableListInnerColumn) => { const rowItem = props.getRowItem?.(props.rowIndex) ?? (props.data as any[])[props.rowIndex]; const item = rowItem as any; const yearDisplay = useMemo(() => { - if (item && 'releaseYear' in item && item.releaseYear != null) { - const releaseYear = item.releaseYear; - const originalYear = - 'originalYear' in item && item.originalYear > 0 ? item.originalYear : null; - - if (originalYear !== null && originalYear !== releaseYear) { - return `${originalYear}${SEPARATOR_STRING}${releaseYear}`; - } - - if (typeof releaseYear === 'number') { - return releaseYear; + if (item && 'year' in item && item.year != null) { + const year = item.year; + if (typeof year === 'number') { + return year; } } return null; 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 17a674a5a..00064e36b 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 @@ -112,6 +112,15 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ value: TableColumn.COMPOSER, width: 300, }, + { + align: 'center', + autoSize: false, + isEnabled: false, + label: i18n.t('table.config.label.date'), + pinned: null, + value: TableColumn.DATE, + width: 240, + }, { align: 'start', autoSize: false, @@ -139,6 +148,15 @@ export const SONG_TABLE_COLUMNS: DefaultTableColumn[] = [ value: TableColumn.YEAR, width: 200, }, + { + align: 'center', + autoSize: false, + isEnabled: false, + label: i18n.t('table.config.label.releaseYear'), + pinned: null, + value: TableColumn.RELEASE_YEAR, + width: 200, + }, { align: 'center', autoSize: false, @@ -414,15 +432,6 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ value: TableColumn.GENRE_BADGE, width: 300, }, - { - align: 'center', - autoSize: false, - isEnabled: true, - label: i18n.t('table.config.label.year'), - pinned: null, - value: TableColumn.YEAR, - width: 200, - }, { align: 'center', autoSize: false, @@ -432,6 +441,15 @@ export const ALBUM_TABLE_COLUMNS: DefaultTableColumn[] = [ value: TableColumn.RELEASE_DATE, width: 240, }, + { + align: 'center', + autoSize: false, + isEnabled: true, + label: i18n.t('table.config.label.releaseYear'), + pinned: null, + value: TableColumn.RELEASE_YEAR, + width: 200, + }, { align: 'center', autoSize: false, diff --git a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx index 167f6462e..4b5c33817 100644 --- a/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx +++ b/src/renderer/components/item-list/item-table-list/item-table-list-column.tsx @@ -48,12 +48,14 @@ import { NumericColumn } from '/@/renderer/components/item-list/item-table-list/ import { PathColumn } from '/@/renderer/components/item-list/item-table-list/columns/path-column'; import { PlaylistReorderColumn } from '/@/renderer/components/item-list/item-table-list/columns/playlist-reorder-column'; import { RatingColumn } from '/@/renderer/components/item-list/item-table-list/columns/rating-column'; +import { ReleaseYearColumn } from '/@/renderer/components/item-list/item-table-list/columns/release-year-column'; import { RowIndexColumn } from '/@/renderer/components/item-list/item-table-list/columns/row-index-column'; import { SizeColumn } from '/@/renderer/components/item-list/item-table-list/columns/size-column'; import { TextColumn } from '/@/renderer/components/item-list/item-table-list/columns/text-column'; import { TitleArtistColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-artist-column'; import { TitleColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-column'; import { TitleCombinedColumn } from '/@/renderer/components/item-list/item-table-list/columns/title-combined-column'; +import { TrackDateColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-date-column'; import { TrackNumberColumn } from '/@/renderer/components/item-list/item-table-list/columns/track-number-column'; import { YearColumn } from '/@/renderer/components/item-list/item-table-list/columns/year-column'; import { useItemDragDropState } from '/@/renderer/components/item-list/item-table-list/hooks/use-item-drag-drop-state'; @@ -248,6 +250,11 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => { case TableColumn.COMPOSER: return ; + case TableColumn.DATE: + return ( + + ); + case TableColumn.DATE_ADDED: return ; @@ -281,6 +288,11 @@ const ItemTableListColumnBase = (props: ItemTableListColumn) => { ); + case TableColumn.RELEASE_YEAR: + return ( + + ); + case TableColumn.ROW_INDEX: return ; @@ -1268,6 +1280,9 @@ export const columnLabelMap: Record = { [TableColumn.COMPOSER]: i18n.t('table.config.label.composer', { postProcess: 'upperCase', }) as string, + [TableColumn.DATE]: i18n.t('table.column.date', { + postProcess: 'upperCase', + }) as string, [TableColumn.DATE_ADDED]: i18n.t('table.column.dateAdded', { postProcess: 'upperCase', }) as string, @@ -1304,6 +1319,9 @@ export const columnLabelMap: Record = { [TableColumn.RELEASE_DATE]: i18n.t('table.column.releaseDate', { postProcess: 'upperCase', }) as string, + [TableColumn.RELEASE_YEAR]: i18n.t('table.column.releaseYear', { + postProcess: 'upperCase', + }) as string, [TableColumn.ROW_INDEX]: ( @@ -1339,7 +1357,7 @@ export const columnLabelMap: Record = { ), - [TableColumn.YEAR]: i18n.t('table.column.releaseYear', { postProcess: 'upperCase' }) as string, + [TableColumn.YEAR]: i18n.t('table.column.year', { postProcess: 'upperCase' }) as string, }; export const ColumnNullFallback = (props: ItemTableListInnerColumn) => { diff --git a/src/renderer/features/albums/components/album-detail-header.tsx b/src/renderer/features/albums/components/album-detail-header.tsx index 2980c26b9..212211aa5 100644 --- a/src/renderer/features/albums/components/album-detail-header.tsx +++ b/src/renderer/features/albums/components/album-detail-header.tsx @@ -132,7 +132,7 @@ export const AlbumDetailHeader = forwardRef((_props, ref) => { if (!album) return []; const originalDifferentFromRelease = - album?.originalDate && album?.originalDate !== album?.releaseDate; + album.originalDate && album.originalDate !== album.releaseDate; const originalYearDifferentFromRelease = album.originalYear > 0 && @@ -141,54 +141,39 @@ export const AlbumDetailHeader = forwardRef((_props, ref) => { const playCount = album?.playCount; - const releasePrefix = originalDifferentFromRelease ? t('page.albumDetail.released') : '♫'; + const trackYearRangeDifferent = album.trackYearRange?.max !== album.trackYearRange?.min; - const releaseYearPrefix = originalYearDifferentFromRelease - ? t('page.albumDetail.released') - : '♫'; + if (album.trackYearRange && trackYearRangeDifferent) { + items.push({ + id: 'trackYearRange', + value: `${album.trackYearRange.min}-${album.trackYearRange.max}`, + }); + } - if (album.originalDate) { - if (originalDifferentFromRelease) { - items.push({ - id: 'originalDate', - value: `♫ ${formatPartialIsoDateUTC(album.originalDate)}`, - }); - } + if (album.originalDate && originalDifferentFromRelease) { + items.push({ + id: 'originalDate', + value: `${formatPartialIsoDateUTC(album.originalDate)}`, + }); + } else if (album.originalYear > 0 && originalYearDifferentFromRelease) { + items.push({ + id: 'originalYear', + value: `${album.originalYear}`, + }); + } - if (releaseDate) { - items.push({ - id: 'releaseDate', - value: `${releasePrefix} ${formatPartialIsoDateUTC(releaseDate)}`, - }); - } - } else if (album.originalYear > 0) { - if (originalYearDifferentFromRelease) { - items.push({ - id: 'originalYear', - value: `♫ ${album.originalYear}`, - }); - } - - if (releaseDate) { - items.push({ - id: 'releaseDate', - value: `${releaseYearPrefix} ${formatPartialIsoDateUTC(releaseDate)}`, - }); - } else if (releaseYear != null && releaseYear > 0) { - items.push({ - id: 'releaseYear', - value: `${releaseYearPrefix} ${releaseYear}`, - }); - } - } else if (releaseDate) { + const prefixNecessary = items.length > 0; + const releasePrefix = prefixNecessary ? `${t('page.albumDetail.released')} ` : ''; + const releaseYearPrefix = prefixNecessary ? `${t('page.albumDetail.released')} ` : ''; + if (releaseDate) { items.push({ id: 'releaseDate', - value: `♫ ${formatPartialIsoDateUTC(releaseDate)}`, + value: `${releasePrefix}${formatPartialIsoDateUTC(releaseDate)}`, }); } else if (releaseYear != null && releaseYear > 0) { items.push({ id: 'releaseYear', - value: `♫ ${releaseYear}`, + value: `${releaseYearPrefix}${releaseYear}`, }); } @@ -278,6 +263,7 @@ export const AlbumDetailHeader = forwardRef((_props, ref) => { {metadataItems.map((item, index) => ( + {index === 0 && } {index > 0 && ( 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 42d3c92ac..8572db474 100644 --- a/src/renderer/features/item-details/components/item-details-modal.tsx +++ b/src/renderer/features/item-details/components/item-details-modal.tsx @@ -6,7 +6,11 @@ import { generatePath, Link } from 'react-router'; import { SongPath } from '/@/renderer/features/item-details/components/song-path'; import { AppRoute } from '/@/renderer/router/routes'; import { formatDurationString, formatSizeString } from '/@/renderer/utils'; -import { formatDateRelative, formatRating } from '/@/renderer/utils/format'; +import { + formatDateRelative, + formatPartialIsoDateUTC, + formatRating, +} from '/@/renderer/utils/format'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; import { normalizeReleaseTypes } from '/@/renderer/utils/normalize-release-types'; import { sanitize } from '/@/renderer/utils/sanitize'; @@ -304,7 +308,24 @@ const SongPropertyMapping: ItemDetailRow[] = [ }, { key: 'discNumber', label: 'common.disc' }, { key: 'trackNumber', label: 'common.trackNumber' }, - { key: 'releaseYear', label: 'filter.releaseYear' }, + { + key: 'date', + label: 'filter.date', + render: (song) => (song.date ? formatPartialIsoDateUTC(song.date) : null), + }, + { + label: 'filter.year', + render: (song) => (!song.date ? song.year : null), + }, + { + key: 'releaseDate', + label: 'filter.releaseDate', + render: (song) => (song.releaseDate ? formatPartialIsoDateUTC(song.releaseDate) : null), + }, + { + label: 'filter.releaseYear', + render: (song) => (!song.releaseDate ? song.releaseYear : null), + }, { label: 'filter.explicitStatus', render: (song, t) => diff --git a/src/renderer/features/player/components/full-screen-player-image.tsx b/src/renderer/features/player/components/full-screen-player-image.tsx index c674d36e8..97d028f0c 100644 --- a/src/renderer/features/player/components/full-screen-player-image.tsx +++ b/src/renderer/features/player/components/full-screen-player-image.tsx @@ -18,6 +18,7 @@ import { usePlayerData, usePlayerSong, } from '/@/renderer/store'; +import { formatPartialIsoDateUTC } from '/@/renderer/utils'; import { Badge } from '/@/shared/components/badge/badge'; import { Center } from '/@/shared/components/center/center'; import { Flex } from '/@/shared/components/flex/flex'; @@ -181,6 +182,7 @@ export const FullScreenPlayerImage = () => { ), codec: currentSong?.container && {currentSong?.container}, + date: currentSong?.date && {formatPartialIsoDateUTC(currentSong?.date)}, disc_number: currentSong?.discNumber && ( {t('common.disc')} {currentSong?.discNumber} @@ -191,7 +193,9 @@ export const FullScreenPlayerImage = () => { currentSong?.genres .slice(0, 2) .map((genre) => {genre.name}), - release_date: currentSong?.releaseDate && {currentSong?.releaseDate}, + release_date: currentSong?.releaseDate && ( + {formatPartialIsoDateUTC(currentSong?.releaseDate)} + ), release_type: currentSong?.tags?.releasetype && ( {currentSong?.tags?.releasetype[0]} ), @@ -202,6 +206,7 @@ export const FullScreenPlayerImage = () => { {t('common.trackNumber')} {currentSong?.trackNumber} ), + year: currentSong?.year && {currentSong?.year}, }; return ( diff --git a/src/renderer/features/player/hooks/use-mpris.ts b/src/renderer/features/player/hooks/use-mpris.ts index bdec1c386..d4913eee2 100644 --- a/src/renderer/features/player/hooks/use-mpris.ts +++ b/src/renderer/features/player/hooks/use-mpris.ts @@ -80,6 +80,7 @@ export const useMPRIS = () => { compilation: null, container: null, createdAt: '', + date: null, discNumber: 0, discSubtitle: null, duration: 0, @@ -109,6 +110,7 @@ export const useMPRIS = () => { updatedAt: new Date().toISOString(), userFavorite: false, userRating: null, + year: null, }; }, [isRadioActive, isRadioPlaying, radioMetadata, stationName]); diff --git a/src/renderer/features/player/utils.ts b/src/renderer/features/player/utils.ts index fba611a8b..2e3f72246 100644 --- a/src/renderer/features/player/utils.ts +++ b/src/renderer/features/player/utils.ts @@ -356,8 +356,10 @@ const getSongFieldValue = (song: Song, field: string): boolean | null | number | return song.playCount; case 'rating': return song.userRating || 0; - case 'year': + case 'releaseYear': return song.releaseYear || 0; + case 'year': + return song.year || 0; default: return null; } diff --git a/src/renderer/features/playlists/utils.ts b/src/renderer/features/playlists/utils.ts index aba659f3f..b12d669ba 100644 --- a/src/renderer/features/playlists/utils.ts +++ b/src/renderer/features/playlists/utils.ts @@ -48,6 +48,7 @@ export function playlistSongsToAlbums(songs: Song[]): PlaylistAlbumRow[] { songCount: null, sortName: song.album ?? '', tags: song.tags, + trackYearRange: null, updatedAt: song.updatedAt, userFavorite: false, userRating: null, diff --git a/src/renderer/features/settings/components/general/fullscreen-player-settings.tsx b/src/renderer/features/settings/components/general/fullscreen-player-settings.tsx index 41a7707db..a729f0f41 100644 --- a/src/renderer/features/settings/components/general/fullscreen-player-settings.tsx +++ b/src/renderer/features/settings/components/general/fullscreen-player-settings.tsx @@ -13,6 +13,7 @@ const PLAYER_ITEMS: Array<[PlayerItem, string]> = [ [PlayerItem.BIT_RATE, 'common.bitrate'], [PlayerItem.BPM, 'common.bpm'], [PlayerItem.CODEC, 'common.codec'], + [PlayerItem.DATE, 'filter.date'], [PlayerItem.DISC_NUMBER, 'table.config.label.discNumber'], [PlayerItem.GENRES, 'entity.genre_other'], [PlayerItem.RELEASE_DATE, 'filter.releaseDate'], @@ -20,6 +21,7 @@ const PLAYER_ITEMS: Array<[PlayerItem, string]> = [ [PlayerItem.RELEASE_YEAR, 'filter.releaseYear'], [PlayerItem.SAMPLE_RATE, 'common.sampleRate'], [PlayerItem.TRACK_NUMBER, 'table.config.label.trackNumber'], + [PlayerItem.YEAR, 'filter.year'], ]; export const FullscreenPlayerSettings = memo(() => { diff --git a/src/renderer/features/settings/components/playback/player-filter-settings.tsx b/src/renderer/features/settings/components/playback/player-filter-settings.tsx index 578a8a631..b9589c468 100644 --- a/src/renderer/features/settings/components/playback/player-filter-settings.tsx +++ b/src/renderer/features/settings/components/playback/player-filter-settings.tsx @@ -66,6 +66,11 @@ const getFilterFields = (t: (key: string, options?: any) => string): FilterField type: 'number', value: 'year', }, + { + label: t('table.config.label.releaseYear'), + type: 'number', + value: 'releaseYear', + }, { label: t('table.config.label.note'), type: 'string', diff --git a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx index 647c644ad..d1d046310 100644 --- a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx +++ b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx @@ -214,6 +214,11 @@ export const CLIENT_SIDE_SONG_FILTERS = [ { defaultOrder: SortOrder.DESC, name: i18n.t('filter.releaseYear'), + value: SongListSort.RELEASE_YEAR, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.year'), value: SongListSort.YEAR, }, ]; @@ -591,6 +596,11 @@ const SONG_LIST_FILTERS: Partial< { defaultOrder: SortOrder.DESC, name: i18n.t('filter.releaseYear'), + value: SongListSort.RELEASE_YEAR, + }, + { + defaultOrder: SortOrder.DESC, + name: i18n.t('filter.year'), value: SongListSort.YEAR, }, ], diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index f28aba7eb..8df6cad83 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -95,10 +95,12 @@ const PlayerItemSchema = z.enum([ 'sample_rate', 'track_number', 'codec', + 'date', 'release_year', 'release_type', 'release_date', 'genres', + 'year', ]); const ArtistItemSchema = z.enum([ @@ -650,6 +652,7 @@ const PlayerFilterFieldSchema = z.enum([ 'duration', 'genre', 'year', + 'releaseYear', 'note', 'path', 'playCount', @@ -967,6 +970,7 @@ export enum PlayerItem { BIT_RATE = 'bit_rate', BPM = 'bpm', CODEC = 'codec', + DATE = 'date', DISC_NUMBER = 'disc_number', GENRES = 'genres', RELEASE_DATE = 'release_date', @@ -974,6 +978,7 @@ export enum PlayerItem { RELEASE_YEAR = 'release_year', SAMPLE_RATE = 'sample_rate', TRACK_NUMBER = 'track_number', + YEAR = 'year', } export enum PlaylistTarget { @@ -1080,6 +1085,10 @@ export const playerItems: SortableItem[] = [ disabled: false, id: PlayerItem.CODEC, }, + { + disabled: true, + id: PlayerItem.DATE, + }, { disabled: true, id: PlayerItem.DISC_NUMBER, @@ -1108,6 +1117,10 @@ export const playerItems: SortableItem[] = [ disabled: true, id: PlayerItem.TRACK_NUMBER, }, + { + disabled: false, + id: PlayerItem.YEAR, + }, ]; export const sidebarItems: SidebarItemType[] = [ @@ -1522,10 +1535,14 @@ const initialState: SettingsState = { alignLeftColumns: [ TableColumn.TITLE, TableColumn.ALBUM_ARTIST, - TableColumn.YEAR, + TableColumn.RELEASE_YEAR, ], columns: ALBUM_TABLE_COLUMNS, - enabledColumns: [TableColumn.TITLE, TableColumn.ALBUM_ARTIST, TableColumn.YEAR], + enabledColumns: [ + TableColumn.TITLE, + TableColumn.ALBUM_ARTIST, + TableColumn.RELEASE_YEAR, + ], pickColumns: [ TableColumn.TITLE, TableColumn.DURATION, @@ -1537,8 +1554,8 @@ const initialState: SettingsState = { TableColumn.PLAY_COUNT, TableColumn.SONG_COUNT, TableColumn.RELEASE_DATE, + TableColumn.RELEASE_YEAR, TableColumn.LAST_PLAYED, - TableColumn.YEAR, ], }), size: 'default', @@ -1599,10 +1616,14 @@ const initialState: SettingsState = { alignLeftColumns: [ TableColumn.TITLE, TableColumn.ALBUM_ARTIST, - TableColumn.YEAR, + TableColumn.RELEASE_YEAR, ], columns: ALBUM_TABLE_COLUMNS, - enabledColumns: [TableColumn.TITLE, TableColumn.ALBUM_ARTIST, TableColumn.YEAR], + enabledColumns: [ + TableColumn.TITLE, + TableColumn.ALBUM_ARTIST, + TableColumn.RELEASE_YEAR, + ], pickColumns: [ TableColumn.TITLE, TableColumn.DURATION, @@ -1614,8 +1635,8 @@ const initialState: SettingsState = { TableColumn.PLAY_COUNT, TableColumn.SONG_COUNT, TableColumn.RELEASE_DATE, + TableColumn.RELEASE_YEAR, TableColumn.LAST_PLAYED, - TableColumn.YEAR, ], }), size: 'default', @@ -1823,15 +1844,17 @@ const initialState: SettingsState = { TableColumn.TITLE, TableColumn.ARTIST, TableColumn.DURATION, - TableColumn.YEAR, TableColumn.BIT_RATE, TableColumn.BPM, TableColumn.CODEC, + TableColumn.DATE, TableColumn.DATE_ADDED, TableColumn.GENRE, TableColumn.LAST_PLAYED, TableColumn.RELEASE_DATE, + TableColumn.RELEASE_YEAR, TableColumn.TRACK_NUMBER, + TableColumn.YEAR, ], }), size: 'default', @@ -1899,15 +1922,17 @@ const initialState: SettingsState = { TableColumn.TITLE, TableColumn.ARTIST, TableColumn.DURATION, - TableColumn.YEAR, TableColumn.BIT_RATE, TableColumn.BPM, TableColumn.CODEC, + TableColumn.DATE, TableColumn.DATE_ADDED, TableColumn.GENRE, TableColumn.LAST_PLAYED, TableColumn.RELEASE_DATE, + TableColumn.RELEASE_YEAR, TableColumn.TRACK_NUMBER, + TableColumn.YEAR, ], }), size: 'default', @@ -2714,6 +2739,92 @@ export const useSettingsStore = createWithEqualityFn()( ); } + if (version < 29) { + const dateColumn: ItemTableListColumnConfig = { + align: 'center', + autoSize: false, + id: TableColumn.DATE, + isEnabled: false, + pinned: null, + width: 240, + }; + const yearColumn: ItemTableListColumnConfig = { + align: 'center', + autoSize: false, + id: TableColumn.YEAR, + isEnabled: false, + pinned: null, + width: 200, + }; + + const listKeysToUpdate: ItemListKey[] = [ + ItemListKey.SONG, + ItemListKey.ALBUM_DETAIL, + ItemListKey.FOLDER, + ItemListKey.PLAYLIST_SONG, + ItemListKey.ALBUM_ARTIST_SONG, + ItemListKey.GENRE_SONG, + ItemListKey.QUEUE_SONG, + ItemListKey.FULL_SCREEN, + ItemListKey.SIDE_QUEUE, + ]; + + listKeysToUpdate.forEach((listKey) => { + const listConfig = state.lists[listKey]; + if (listConfig?.table?.columns) { + const columns = listConfig.table.columns; + const hasYear = columns.some((col) => col.id === TableColumn.YEAR); + if (!hasYear) { + const releaseYearIndex = columns.findIndex( + (col) => col.id === TableColumn.RELEASE_YEAR, + ); + if (releaseYearIndex >= 0) { + columns.splice(releaseYearIndex, 0, yearColumn); + } else { + columns.push(yearColumn); + } + } + const hasDate = columns.some((col) => col.id === TableColumn.DATE); + if (!hasDate) { + const releaseDateIndex = columns.findIndex( + (col) => col.id === TableColumn.RELEASE_DATE, + ); + if (releaseDateIndex >= 0) { + columns.splice(releaseDateIndex, 0, dateColumn); + } else { + columns.push(dateColumn); + } + } + } + }); + const listConfig = state.lists[ItemListKey.ALBUM]; + if (listConfig?.detail?.columns) { + const columns = listConfig.detail.columns; + const hasYear = columns.some((col) => col.id === TableColumn.YEAR); + if (!hasYear) { + const releaseYearIndex = columns.findIndex( + (col) => col.id === TableColumn.RELEASE_YEAR, + ); + if (releaseYearIndex >= 0) { + columns.splice(releaseYearIndex, 0, yearColumn); + } else { + columns.push(yearColumn); + } + } + const hasDate = columns.some((col) => col.id === TableColumn.DATE); + if (!hasDate) { + const releaseDateIndex = columns.findIndex( + (col) => col.id === TableColumn.RELEASE_DATE, + ); + if (releaseDateIndex >= 0) { + columns.splice(releaseDateIndex, 0, dateColumn); + } else { + columns.push(dateColumn); + } + } + } + } + if (version < 30) { for (const [key, displaySettings] of Object.entries(state.lyricsDisplay)) { const legacySettings = displaySettings as typeof displaySettings & { diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index 8a31a5799..9b9e8ce41 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -155,6 +155,7 @@ const normalizeSong = ( compilation: null, container, createdAt: item.DateCreated, + date: releaseDate || String(releaseYear), discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1, discSubtitle: null, duration: item.RunTimeTicks / TICKS_PER_MS, @@ -209,6 +210,7 @@ const normalizeSong = ( updatedAt: item.DateCreated, userFavorite: (item.UserData && item.UserData.IsFavorite) || false, userRating: null, + year: releaseYear, }; }; @@ -280,6 +282,7 @@ const normalizeAlbum = ( songs: item.Songs?.map((song) => normalizeSong(song, server)), sortName: item.SortName || item.Name, tags: getTags(item), + trackYearRange: null, updatedAt: item?.DateLastMediaAdded || item.DateCreated, userFavorite: item.UserData?.IsFavorite || false, userRating: null, diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index ad9b93bf8..10e6160d9 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -212,9 +212,13 @@ const normalizeSong = ( const fromSongRelease = parsePartialIsoDate(item.releaseDate); const songApiYear = coerceYear(item.year); + const fromSongDate = parsePartialIsoDate(item.date); const releaseYear: null | number = fromSongRelease.year > 0 ? fromSongRelease.year : songApiYear > 0 ? songApiYear : null; - const releaseDate = fromSongRelease.date ?? (songApiYear > 0 ? String(songApiYear) : null); + const releaseDate = + fromSongRelease.date ?? fromSongDate.date ?? (songApiYear > 0 ? String(songApiYear) : null); + const date = fromSongDate.date ?? (songApiYear > 0 ? String(songApiYear) : null); + const year = fromSongDate.year > 0 ? fromSongDate.year : releaseYear; return { album: item.album, @@ -233,6 +237,7 @@ const normalizeSong = ( compilation: item.compilation, container: item.suffix, createdAt: item.createdAt, + date, discNumber: item.discNumber, discSubtitle: item.discSubtitle ? item.discSubtitle : null, duration: item.duration * 1000, @@ -285,6 +290,7 @@ const normalizeSong = ( updatedAt: item.updatedAt, userFavorite: item.starred || false, userRating: item.rating || null, + year, }; }; @@ -337,6 +343,7 @@ const normalizeAlbum = ( ): Album => { const releaseDate = normalizeNavidromeReleaseDate(item); const originalDate = normalizeNavidromeOriginalDate(item); + const trackYearRange = { max: item.maxYear, min: item.minYear }; return { ...parseAlbumTags(item), @@ -384,6 +391,7 @@ const normalizeAlbum = ( songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined, sortName: item.orderAlbumName, tags: item.tags || null, + trackYearRange, updatedAt: item.updatedAt, userFavorite: item.starred || false, userRating: item.rating || null, diff --git a/src/shared/api/navidrome/navidrome-types.ts b/src/shared/api/navidrome/navidrome-types.ts index 6ac02acae..c519124eb 100644 --- a/src/shared/api/navidrome/navidrome-types.ts +++ b/src/shared/api/navidrome/navidrome-types.ts @@ -59,6 +59,7 @@ export enum NDSongListSort { RANDOM = 'random', RATING = 'rating', RECENTLY_ADDED = 'createdAt', + RELEASE_YEAR = 'releaseYear', TITLE = 'title', TRACK = 'track', YEAR = 'year', @@ -555,6 +556,7 @@ const song = z.object({ comment: z.string().optional(), compilation: z.boolean(), createdAt: z.string(), + date: z.string().optional(), discNumber: z.number(), discSubtitle: z.string().optional(), duration: z.number(), diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index e852a55fe..bf9ddd28d 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -158,6 +158,16 @@ const subsonicReleaseFields = (item: { return { releaseDate: null, releaseYear: null }; }; +const subsonicTrackYearField = (item: { + year?: number; +}): { date: null | string; year: null | number } => { + const y = coerceYear(item.year); + if (y > 0) { + return { date: String(y), year: y }; + } + + return { date: null, year: null }; +}; const normalizeSong = ( item: z.infer, @@ -173,6 +183,7 @@ const normalizeSong = ( : item.artist || ''; const { releaseDate, releaseYear } = subsonicReleaseFields(item); + const { date, year } = subsonicTrackYearField(item); return { _itemType: LibraryItem.SONG, @@ -192,6 +203,7 @@ const normalizeSong = ( compilation: null, container: item.contentType.startsWith('audio/') ? item.contentType.split('/')[1] : null, createdAt: item.created, + date, discNumber: item.discNumber || 1, discSubtitle: discTitleMap?.get(item.discNumber ?? 1) ?? null, duration: item.duration ? item.duration * 1000 : 0, @@ -239,6 +251,7 @@ const normalizeSong = ( updatedAt: '', userFavorite: Boolean(item.starred) || false, userRating: item.userRating || null, + year, }; }; @@ -353,6 +366,7 @@ const normalizeAlbum = ( ) || [], sortName: item.title, tags: null, + trackYearRange: null, updatedAt: item.created, userFavorite: Boolean(item.starred) || false, userRating: item.userRating || null, diff --git a/src/shared/api/utils.ts b/src/shared/api/utils.ts index a344bbe86..5a5aad556 100644 --- a/src/shared/api/utils.ts +++ b/src/shared/api/utils.ts @@ -311,6 +311,14 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor ); break; + case SongListSort.RELEASE_YEAR: + results = orderBy( + results, + ['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'], + [order, order, order, order], + ); + break; + case SongListSort.SORT_NAME: results = orderBy(results, [(v) => v.sortName ?? v.name], [order]); break; @@ -318,7 +326,7 @@ export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: Sor case SongListSort.YEAR: results = orderBy( results, - ['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'], + ['year', (v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'], [order, order, order, order], ); break; diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index c89a81aba..104e08da8 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -207,6 +207,7 @@ export type Album = { songs?: Song[]; sortName: string; tags: null | Record; + trackYearRange: null | { max: number; min: number }; updatedAt: string; userFavorite: boolean; userRating: null | number; @@ -387,6 +388,7 @@ export type Song = { compilation: boolean | null; container: null | string; createdAt: string; + date: null | PartialIsoDateString; discNumber: number; discSubtitle: null | string; duration: number; @@ -417,6 +419,7 @@ export type Song = { updatedAt: string; userFavorite: boolean; userRating: null | number; + year: null | number; }; // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -620,6 +623,7 @@ export enum SongListSort { RECENTLY_ADDED = 'recentlyAdded', RECENTLY_PLAYED = 'recentlyPlayed', RELEASE_DATE = 'releaseDate', + RELEASE_YEAR = 'releaseYear', SORT_NAME = 'sortName', YEAR = 'year', } @@ -688,6 +692,7 @@ export const songListSortMap: SongListSortMap = { recentlyAdded: JFSongListSort.RECENTLY_ADDED, recentlyPlayed: JFSongListSort.RECENTLY_PLAYED, releaseDate: JFSongListSort.RELEASE_DATE, + releaseYear: undefined, sortName: JFSongListSort.NAME, year: undefined, }, @@ -710,6 +715,7 @@ export const songListSortMap: SongListSortMap = { recentlyAdded: NDSongListSort.RECENTLY_ADDED, recentlyPlayed: NDSongListSort.PLAY_DATE, releaseDate: undefined, + releaseYear: NDSongListSort.RELEASE_YEAR, sortName: NDSongListSort.TITLE, year: NDSongListSort.YEAR, }, @@ -732,6 +738,7 @@ export const songListSortMap: SongListSortMap = { recentlyAdded: undefined, recentlyPlayed: undefined, releaseDate: undefined, + releaseYear: undefined, sortName: undefined, year: undefined, }, diff --git a/src/shared/types/types.ts b/src/shared/types/types.ts index 6168343d4..9038269d7 100644 --- a/src/shared/types/types.ts +++ b/src/shared/types/types.ts @@ -178,6 +178,7 @@ export enum TableColumn { CODEC = 'container', COMMENT = 'comment', COMPOSER = 'composer', + DATE = 'date', DATE_ADDED = 'createdAt', DISC_NUMBER = 'discNumber', DURATION = 'duration', @@ -192,6 +193,7 @@ export enum TableColumn { PLAY_COUNT = 'playCount', PLAYLIST_REORDER = 'playlistReorder', RELEASE_DATE = 'releaseDate', + RELEASE_YEAR = 'releaseYear', ROW_INDEX = 'rowIndex', SAMPLE_RATE = 'sampleRate', SIZE = 'size', @@ -203,7 +205,7 @@ export enum TableColumn { TRACK_NUMBER = 'trackNumber', USER_FAVORITE = 'userFavorite', USER_RATING = 'userRating', - YEAR = 'releaseYear', + YEAR = 'year', } export type DiscoveredServerItem = {