[enhancement/localization]: sort navidrome albums by year, add more language keys
This commit is contained in:
parent
86a93866d0
commit
24bf7ae31f
7 changed files with 56 additions and 22 deletions
|
@ -86,7 +86,7 @@ First thing to do is check that your MPV binary path is correct. Navigate to the
|
|||
|
||||
Feishin supports any music server that implements a [Navidrome](https://www.navidrome.org/) or [Jellyfin](https://jellyfin.org/) API. **Subsonic API is not currently supported**. This will likely be added in [later when the new Subsonic API is decided on](https://support.symfonium.app/t/subsonic-servers-participation/1233).
|
||||
|
||||
- [Navidrome](https://github.com/navidrome/navidrome)
|
||||
- [Navidrome](https://github.com/navidrome/navidrome) version 0.48.0 and newer
|
||||
- [Jellyfin](https://github.com/jellyfin/jellyfin)
|
||||
- [Funkwhale](https://funkwhale.audio/) - TBD
|
||||
- Subsonic-compatible servers - TBD
|
||||
|
|
|
@ -254,6 +254,17 @@
|
|||
}
|
||||
},
|
||||
"page": {
|
||||
"albumArtistDetail": {
|
||||
"about": "About {{artist}}",
|
||||
"appearsOn": "appears on",
|
||||
"recentReleases": "recent releases",
|
||||
"viewDiscography": "view discography",
|
||||
"relatedArtists": "related $t(entity.artist_other)",
|
||||
"topSongs": "top songs",
|
||||
"topSongsFrom": "Top songs from {{title}}",
|
||||
"viewAll": "view all",
|
||||
"viewAllTracks": "view all $t(entity.track_other)"
|
||||
},
|
||||
"albumArtistList": {
|
||||
"title": "$t(entity.albumArtist_other)"
|
||||
},
|
||||
|
@ -353,6 +364,8 @@
|
|||
"tracks": "$t(entity.track_other)"
|
||||
},
|
||||
"trackList": {
|
||||
"artistTracks": "Tracks by {{artist}}",
|
||||
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||
"title": "$t(entity.track_other)"
|
||||
}
|
||||
},
|
||||
|
|
|
@ -421,7 +421,8 @@ export const albumListSortMap: AlbumListSortMap = {
|
|||
rating: NDAlbumListSort.RATING,
|
||||
recentlyAdded: NDAlbumListSort.RECENTLY_ADDED,
|
||||
recentlyPlayed: NDAlbumListSort.PLAY_DATE,
|
||||
releaseDate: undefined,
|
||||
// Recent versions of Navidrome support release date, but fallback to year for now
|
||||
releaseDate: NDAlbumListSort.YEAR,
|
||||
songCount: NDAlbumListSort.SONG_COUNT,
|
||||
year: NDAlbumListSort.YEAR,
|
||||
},
|
||||
|
|
|
@ -213,7 +213,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Recent releases
|
||||
{t('page.albumArtistDetail.recentReleases', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<Button
|
||||
compact
|
||||
|
@ -222,7 +224,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
{t('page.albumArtistDetail.viewDiscography')}
|
||||
</Button>
|
||||
</Group>
|
||||
),
|
||||
|
@ -238,7 +240,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Appears on
|
||||
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
|
||||
</TextTitle>
|
||||
),
|
||||
uniqueId: 'compilationAlbums',
|
||||
|
@ -252,7 +254,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Related artists
|
||||
{t('page.albumArtistDetail.relatedArtists', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
),
|
||||
uniqueId: 'similarArtists',
|
||||
|
@ -267,6 +271,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
recentAlbumsQuery?.data?.items,
|
||||
recentAlbumsQuery.isFetching,
|
||||
recentAlbumsQuery?.isLoading,
|
||||
t,
|
||||
]);
|
||||
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
@ -381,7 +386,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
to={artistDiscographyLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View discography
|
||||
{t('page.albumArtistDetail.viewDiscography')}
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
|
@ -390,7 +395,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
to={artistSongsLink}
|
||||
variant="subtle"
|
||||
>
|
||||
View all songs
|
||||
{t('page.albumArtistDetail.viewAllTracks')}
|
||||
</Button>
|
||||
</Group>
|
||||
{showGenres ? (
|
||||
|
@ -463,7 +468,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
About {detailQuery?.data?.name}
|
||||
{t('page.albumArtistDetail.about', {
|
||||
artist: detailQuery?.data?.name,
|
||||
})}
|
||||
</TextTitle>
|
||||
<Spoiler
|
||||
dangerouslySetInnerHTML={{ __html: detailQuery?.data?.biography || '' }}
|
||||
|
@ -484,7 +491,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
order={2}
|
||||
weight={700}
|
||||
>
|
||||
Top Songs
|
||||
{t('page.albumArtistDetail.topSongs', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</TextTitle>
|
||||
<Button
|
||||
compact
|
||||
|
@ -498,7 +507,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||
)}
|
||||
variant="subtle"
|
||||
>
|
||||
View all
|
||||
{t('page.albumArtistDetail.viewAll', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
|
|
|
@ -33,7 +33,9 @@ export const AlbumArtistDetailTopSongsListHeader = ({
|
|||
<PageHeader p="1rem">
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<LibraryHeaderBar.Title>Top songs from {title}</LibraryHeaderBar.Title>
|
||||
<LibraryHeaderBar.Title>
|
||||
{t('page.albumArtistDetail.topSongsFrom', { title })}
|
||||
</LibraryHeaderBar.Title>
|
||||
<Paper
|
||||
fw="600"
|
||||
px="1rem"
|
||||
|
@ -57,7 +59,7 @@ export const AlbumArtistDetailTopSongsListHeader = ({
|
|||
icon={<RiPlayFill />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
>
|
||||
{t('player.add', { postProcess: 'sentenceCase' })}
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddBoxFill />}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { ChangeEvent, MutableRefObject } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import { Flex, Group, Stack } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, MutableRefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath, Link, useParams, useSearchParams } from 'react-router-dom';
|
||||
import { useCurrentServer } from '../../../store/auth.store';
|
||||
import { LibraryItem } from '/@/renderer/api/types';
|
||||
|
@ -17,6 +18,7 @@ interface SearchHeaderProps {
|
|||
}
|
||||
|
||||
export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { itemType } = useParams() as { itemType: LibraryItem };
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const cq = useContainerQuery();
|
||||
|
@ -70,7 +72,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
|||
}}
|
||||
variant={itemType === LibraryItem.SONG ? 'filled' : 'subtle'}
|
||||
>
|
||||
Tracks
|
||||
{t('entity.track_other', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
|
@ -87,7 +89,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
|||
}}
|
||||
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'}
|
||||
>
|
||||
Albums
|
||||
{t('entity.album_other', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
|
@ -104,7 +106,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
|||
}}
|
||||
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'}
|
||||
>
|
||||
Artists
|
||||
{t('entity.artist_other', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</FilterBar>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams, useSearchParams } from 'react-router-dom';
|
||||
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types';
|
||||
import { ListContext } from '/@/renderer/context/list-context';
|
||||
|
@ -16,6 +17,7 @@ import { titleCase } from '/@/renderer/utils';
|
|||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
|
||||
const TrackListRoute = () => {
|
||||
const { t } = useTranslation();
|
||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const server = useCurrentServer();
|
||||
|
@ -132,6 +134,13 @@ const TrackListRoute = () => {
|
|||
};
|
||||
}, [albumArtistId, customFilters, genreId, handlePlay, pageKey]);
|
||||
|
||||
const artist = searchParams.get('artistName');
|
||||
const title = artist
|
||||
? t('page.trackList.artistTracks', { artist })
|
||||
: genreId
|
||||
? t('page.trackList.genreTracks', { genre: titleCase(genreTitle) })
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<AnimatedPage>
|
||||
<ListContext.Provider value={providerValue}>
|
||||
|
@ -139,11 +148,7 @@ const TrackListRoute = () => {
|
|||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
tableRef={tableRef}
|
||||
title={
|
||||
searchParams.get('artistName') || genreId
|
||||
? `"${titleCase(genreTitle)}" Tracks`
|
||||
: undefined
|
||||
}
|
||||
title={title}
|
||||
/>
|
||||
<SongListContent
|
||||
gridRef={gridRef}
|
||||
|
|
Reference in a new issue