[enhancement/localization]: sort navidrome albums by year, add more language keys

This commit is contained in:
Kendall Garner 2024-02-03 15:05:33 -08:00
parent 86a93866d0
commit 24bf7ae31f
No known key found for this signature in database
GPG key ID: 18D2767419676C87
7 changed files with 56 additions and 22 deletions

View file

@ -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

View file

@ -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)"
} }
}, },

View file

@ -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,
}, },

View file

@ -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>

View file

@ -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 />}

View file

@ -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>

View file

@ -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}