[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).
|
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)
|
- [Jellyfin](https://github.com/jellyfin/jellyfin)
|
||||||
- [Funkwhale](https://funkwhale.audio/) - TBD
|
- [Funkwhale](https://funkwhale.audio/) - TBD
|
||||||
- Subsonic-compatible servers - TBD
|
- Subsonic-compatible servers - TBD
|
||||||
|
|
|
@ -254,6 +254,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"page": {
|
"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": {
|
"albumArtistList": {
|
||||||
"title": "$t(entity.albumArtist_other)"
|
"title": "$t(entity.albumArtist_other)"
|
||||||
},
|
},
|
||||||
|
@ -353,6 +364,8 @@
|
||||||
"tracks": "$t(entity.track_other)"
|
"tracks": "$t(entity.track_other)"
|
||||||
},
|
},
|
||||||
"trackList": {
|
"trackList": {
|
||||||
|
"artistTracks": "Tracks by {{artist}}",
|
||||||
|
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
|
||||||
"title": "$t(entity.track_other)"
|
"title": "$t(entity.track_other)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -421,7 +421,8 @@ export const albumListSortMap: AlbumListSortMap = {
|
||||||
rating: NDAlbumListSort.RATING,
|
rating: NDAlbumListSort.RATING,
|
||||||
recentlyAdded: NDAlbumListSort.RECENTLY_ADDED,
|
recentlyAdded: NDAlbumListSort.RECENTLY_ADDED,
|
||||||
recentlyPlayed: NDAlbumListSort.PLAY_DATE,
|
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,
|
songCount: NDAlbumListSort.SONG_COUNT,
|
||||||
year: NDAlbumListSort.YEAR,
|
year: NDAlbumListSort.YEAR,
|
||||||
},
|
},
|
||||||
|
|
|
@ -213,7 +213,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
order={2}
|
order={2}
|
||||||
weight={700}
|
weight={700}
|
||||||
>
|
>
|
||||||
Recent releases
|
{t('page.albumArtistDetail.recentReleases', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
@ -222,7 +224,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
to={artistDiscographyLink}
|
to={artistDiscographyLink}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
View discography
|
{t('page.albumArtistDetail.viewDiscography')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
|
@ -238,7 +240,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
order={2}
|
order={2}
|
||||||
weight={700}
|
weight={700}
|
||||||
>
|
>
|
||||||
Appears on
|
{t('page.albumArtistDetail.appearsOn', { postProcess: 'sentenceCase' })}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
),
|
),
|
||||||
uniqueId: 'compilationAlbums',
|
uniqueId: 'compilationAlbums',
|
||||||
|
@ -252,7 +254,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
order={2}
|
order={2}
|
||||||
weight={700}
|
weight={700}
|
||||||
>
|
>
|
||||||
Related artists
|
{t('page.albumArtistDetail.relatedArtists', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
),
|
),
|
||||||
uniqueId: 'similarArtists',
|
uniqueId: 'similarArtists',
|
||||||
|
@ -267,6 +271,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
recentAlbumsQuery?.data?.items,
|
recentAlbumsQuery?.data?.items,
|
||||||
recentAlbumsQuery.isFetching,
|
recentAlbumsQuery.isFetching,
|
||||||
recentAlbumsQuery?.isLoading,
|
recentAlbumsQuery?.isLoading,
|
||||||
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const playButtonBehavior = usePlayButtonBehavior();
|
const playButtonBehavior = usePlayButtonBehavior();
|
||||||
|
@ -381,7 +386,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
to={artistDiscographyLink}
|
to={artistDiscographyLink}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
View discography
|
{t('page.albumArtistDetail.viewDiscography')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
@ -390,7 +395,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
to={artistSongsLink}
|
to={artistSongsLink}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
View all songs
|
{t('page.albumArtistDetail.viewAllTracks')}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
{showGenres ? (
|
{showGenres ? (
|
||||||
|
@ -463,7 +468,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
order={2}
|
order={2}
|
||||||
weight={700}
|
weight={700}
|
||||||
>
|
>
|
||||||
About {detailQuery?.data?.name}
|
{t('page.albumArtistDetail.about', {
|
||||||
|
artist: detailQuery?.data?.name,
|
||||||
|
})}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
<Spoiler
|
<Spoiler
|
||||||
dangerouslySetInnerHTML={{ __html: detailQuery?.data?.biography || '' }}
|
dangerouslySetInnerHTML={{ __html: detailQuery?.data?.biography || '' }}
|
||||||
|
@ -484,7 +491,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
order={2}
|
order={2}
|
||||||
weight={700}
|
weight={700}
|
||||||
>
|
>
|
||||||
Top Songs
|
{t('page.albumArtistDetail.topSongs', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
</TextTitle>
|
</TextTitle>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
@ -498,7 +507,9 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
||||||
)}
|
)}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
View all
|
{t('page.albumArtistDetail.viewAll', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
|
@ -33,7 +33,9 @@ export const AlbumArtistDetailTopSongsListHeader = ({
|
||||||
<PageHeader p="1rem">
|
<PageHeader p="1rem">
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||||
<LibraryHeaderBar.Title>Top songs from {title}</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Title>
|
||||||
|
{t('page.albumArtistDetail.topSongsFrom', { title })}
|
||||||
|
</LibraryHeaderBar.Title>
|
||||||
<Paper
|
<Paper
|
||||||
fw="600"
|
fw="600"
|
||||||
px="1rem"
|
px="1rem"
|
||||||
|
@ -57,7 +59,7 @@ export const AlbumArtistDetailTopSongsListHeader = ({
|
||||||
icon={<RiPlayFill />}
|
icon={<RiPlayFill />}
|
||||||
onClick={() => handlePlay(Play.NOW)}
|
onClick={() => handlePlay(Play.NOW)}
|
||||||
>
|
>
|
||||||
{t('player.add', { postProcess: 'sentenceCase' })}
|
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
icon={<RiAddBoxFill />}
|
icon={<RiAddBoxFill />}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
import { ChangeEvent, MutableRefObject } from 'react';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import { Flex, Group, Stack } from '@mantine/core';
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
import debounce from 'lodash/debounce';
|
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 { generatePath, Link, useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { useCurrentServer } from '../../../store/auth.store';
|
import { useCurrentServer } from '../../../store/auth.store';
|
||||||
import { LibraryItem } from '/@/renderer/api/types';
|
import { LibraryItem } from '/@/renderer/api/types';
|
||||||
|
@ -17,6 +18,7 @@ interface SearchHeaderProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { itemType } = useParams() as { itemType: LibraryItem };
|
const { itemType } = useParams() as { itemType: LibraryItem };
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
|
@ -70,7 +72,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
||||||
}}
|
}}
|
||||||
variant={itemType === LibraryItem.SONG ? 'filled' : 'subtle'}
|
variant={itemType === LibraryItem.SONG ? 'filled' : 'subtle'}
|
||||||
>
|
>
|
||||||
Tracks
|
{t('entity.track_other', { postProcess: 'sentenceCase' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
@ -87,7 +89,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
||||||
}}
|
}}
|
||||||
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'}
|
variant={itemType === LibraryItem.ALBUM ? 'filled' : 'subtle'}
|
||||||
>
|
>
|
||||||
Albums
|
{t('entity.album_other', { postProcess: 'sentenceCase' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
@ -104,7 +106,7 @@ export const SearchHeader = ({ tableRef, navigationId }: SearchHeaderProps) => {
|
||||||
}}
|
}}
|
||||||
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'}
|
variant={itemType === LibraryItem.ALBUM_ARTIST ? 'filled' : 'subtle'}
|
||||||
>
|
>
|
||||||
Artists
|
{t('entity.artist_other', { postProcess: 'sentenceCase' })}
|
||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
import { useParams, useSearchParams } from 'react-router-dom';
|
||||||
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types';
|
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/renderer/api/types';
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
|
@ -16,6 +17,7 @@ import { titleCase } from '/@/renderer/utils';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||||
|
|
||||||
const TrackListRoute = () => {
|
const TrackListRoute = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
@ -132,6 +134,13 @@ const TrackListRoute = () => {
|
||||||
};
|
};
|
||||||
}, [albumArtistId, customFilters, genreId, handlePlay, pageKey]);
|
}, [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 (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
|
@ -139,11 +148,7 @@ const TrackListRoute = () => {
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
title={
|
title={title}
|
||||||
searchParams.get('artistName') || genreId
|
|
||||||
? `"${titleCase(genreTitle)}" Tracks`
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<SongListContent
|
<SongListContent
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
|
|
Reference in a new issue