mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
add favorites list
This commit is contained in:
@@ -363,6 +363,9 @@
|
|||||||
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
|
||||||
"title": "$t(entity.album_other)"
|
"title": "$t(entity.album_other)"
|
||||||
},
|
},
|
||||||
|
"favorites": {
|
||||||
|
"title": "$t(entity.favorite_other)"
|
||||||
|
},
|
||||||
"appMenu": {
|
"appMenu": {
|
||||||
"collapseSidebar": "collapse sidebar",
|
"collapseSidebar": "collapse sidebar",
|
||||||
"expandSidebar": "expand sidebar",
|
"expandSidebar": "expand sidebar",
|
||||||
@@ -497,6 +500,7 @@
|
|||||||
"albumArtists": "$t(entity.albumArtist_other)",
|
"albumArtists": "$t(entity.albumArtist_other)",
|
||||||
"albums": "$t(entity.album_other)",
|
"albums": "$t(entity.album_other)",
|
||||||
"artists": "$t(entity.artist_other)",
|
"artists": "$t(entity.artist_other)",
|
||||||
|
"favorites": "$t(entity.favorite_other)",
|
||||||
"folders": "$t(entity.folder_other)",
|
"folders": "$t(entity.folder_other)",
|
||||||
"genres": "$t(entity.genre_other)",
|
"genres": "$t(entity.genre_other)",
|
||||||
"home": "$t(common.home)",
|
"home": "$t(common.home)",
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
library_id: getLibraryId(query.musicFolderId),
|
library_id: getLibraryId(query.musicFolderId),
|
||||||
name: query.searchTerm,
|
name: query.searchTerm,
|
||||||
|
starred: query.favorite,
|
||||||
...query._custom,
|
...query._custom,
|
||||||
role: hasFeature(apiClientProps.server, ServerFeature.BFR) ? 'albumartist' : '',
|
role: hasFeature(apiClientProps.server, ServerFeature.BFR) ? 'albumartist' : '',
|
||||||
...excludeMissing(apiClientProps.server),
|
...excludeMissing(apiClientProps.server),
|
||||||
@@ -330,6 +331,7 @@ export const NavidromeController: InternalControllerEndpoint = {
|
|||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
library_id: getLibraryId(query.musicFolderId),
|
library_id: getLibraryId(query.musicFolderId),
|
||||||
name: query.searchTerm,
|
name: query.searchTerm,
|
||||||
|
starred: query.favorite,
|
||||||
...query._custom,
|
...query._custom,
|
||||||
role: query.role || undefined,
|
role: query.role || undefined,
|
||||||
...excludeMissing(apiClientProps.server),
|
...excludeMissing(apiClientProps.server),
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||||
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||||
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||||
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { AlbumArtistListSort } from '/@/shared/types/domain-types';
|
import { AlbumArtistListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const useAlbumArtistListFilters = () => {
|
export const useAlbumArtistListFilters = () => {
|
||||||
const { sortBy } = useSortByFilter<AlbumArtistListSort>(null, ItemListKey.ALBUM_ARTIST);
|
const { setSortBy, sortBy } = useSortByFilter<AlbumArtistListSort>(
|
||||||
|
null,
|
||||||
|
ItemListKey.ALBUM_ARTIST,
|
||||||
|
);
|
||||||
|
|
||||||
const { sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST);
|
const { setSortOrder, sortOrder } = useSortOrderFilter(null, ItemListKey.ALBUM_ARTIST);
|
||||||
|
|
||||||
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
setSearchTerm(null);
|
||||||
|
setSortBy(AlbumArtistListSort.NAME);
|
||||||
|
setSortOrder(SortOrder.ASC);
|
||||||
|
}, [setSearchTerm, setSortBy, setSortOrder]);
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||||
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||||
@@ -19,6 +30,7 @@ export const useAlbumArtistListFilters = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
clear,
|
||||||
query,
|
query,
|
||||||
setSearchTerm,
|
setSearchTerm,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
import {
|
||||||
|
AlbumListView,
|
||||||
|
OverrideAlbumListQuery,
|
||||||
|
} from '/@/renderer/features/albums/components/album-list-content';
|
||||||
|
import {
|
||||||
|
AlbumArtistListView,
|
||||||
|
OverrideAlbumArtistListQuery,
|
||||||
|
} from '/@/renderer/features/artists/components/album-artist-list-content';
|
||||||
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
|
import {
|
||||||
|
OverrideSongListQuery,
|
||||||
|
SongListView,
|
||||||
|
} from '/@/renderer/features/songs/components/song-list-content';
|
||||||
|
import { useListSettings } from '/@/renderer/store';
|
||||||
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface FavoritesContentProps {
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FavoritesContent = ({ itemType }: FavoritesContentProps) => {
|
||||||
|
return (
|
||||||
|
<AnimatedPage>
|
||||||
|
<Suspense fallback={<Spinner container />}>
|
||||||
|
{itemType === LibraryItem.ALBUM && <AlbumFavorites />}
|
||||||
|
{itemType === LibraryItem.SONG && <SongFavorites />}
|
||||||
|
{itemType === LibraryItem.ALBUM_ARTIST && <ArtistFavorites />}
|
||||||
|
</Suspense>
|
||||||
|
</AnimatedPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AlbumFavorites = () => {
|
||||||
|
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ALBUM);
|
||||||
|
const { customFilters } = useListContext();
|
||||||
|
|
||||||
|
const albumQuery: OverrideAlbumListQuery = {
|
||||||
|
...(customFilters as OverrideAlbumListQuery),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlbumListView
|
||||||
|
display={display}
|
||||||
|
grid={grid}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
overrideQuery={albumQuery}
|
||||||
|
pagination={pagination}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SongFavorites = () => {
|
||||||
|
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.SONG);
|
||||||
|
const { customFilters } = useListContext();
|
||||||
|
|
||||||
|
const songQuery: OverrideSongListQuery = {
|
||||||
|
...(customFilters as OverrideSongListQuery),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SongListView
|
||||||
|
display={display}
|
||||||
|
grid={grid}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
overrideQuery={songQuery}
|
||||||
|
pagination={pagination}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ArtistFavorites = () => {
|
||||||
|
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.ARTIST);
|
||||||
|
const { customFilters } = useListContext();
|
||||||
|
|
||||||
|
const albumArtistQuery: OverrideAlbumArtistListQuery = {
|
||||||
|
...(customFilters as OverrideAlbumArtistListQuery),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AlbumArtistListView
|
||||||
|
display={display}
|
||||||
|
grid={grid}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
overrideQuery={albumArtistQuery}
|
||||||
|
pagination={pagination}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
|
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
||||||
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
|
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
|
||||||
|
import { useAlbumArtistListFilters } from '/@/renderer/features/artists/hooks/use-album-artist-list-filters';
|
||||||
|
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
||||||
|
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||||
|
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
|
||||||
|
import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters';
|
||||||
|
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||||
|
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||||
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
interface FavoritesHeaderProps {
|
||||||
|
itemType: LibraryItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FavoritesHeader = ({ itemType }: FavoritesHeaderProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { customFilters, itemCount } = useListContext();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const albumFilters = useAlbumListFilters();
|
||||||
|
const albumArtistFilters = useAlbumArtistListFilters();
|
||||||
|
const songFilters = useSongListFilters();
|
||||||
|
|
||||||
|
const playQuery = useMemo(() => {
|
||||||
|
let query = {};
|
||||||
|
switch (itemType) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
|
query = albumFilters.query;
|
||||||
|
break;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
query = albumArtistFilters.query;
|
||||||
|
break;
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
query = songFilters.query;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...query,
|
||||||
|
...(customFilters ?? {}),
|
||||||
|
};
|
||||||
|
}, [albumFilters.query, albumArtistFilters.query, songFilters.query, customFilters, itemType]);
|
||||||
|
|
||||||
|
const handleItemTypeChange = useCallback(
|
||||||
|
(type: LibraryItem) => {
|
||||||
|
albumFilters.clear();
|
||||||
|
songFilters.clear();
|
||||||
|
albumArtistFilters.clear();
|
||||||
|
|
||||||
|
// Clear all URL search params except 'type'
|
||||||
|
navigate(`?type=${type}`, { replace: true });
|
||||||
|
},
|
||||||
|
[albumFilters, albumArtistFilters, songFilters, navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap={0}>
|
||||||
|
<PageHeader>
|
||||||
|
<Flex justify="space-between" w="100%">
|
||||||
|
<LibraryHeaderBar ignoreMaxWidth>
|
||||||
|
<PlayButton itemType={itemType} query={playQuery} />
|
||||||
|
<LibraryHeaderBar.Title>
|
||||||
|
<DropdownMenu position="right">
|
||||||
|
<DropdownMenu.Target>
|
||||||
|
<Stack gap={0} style={{ cursor: 'pointer' }}>
|
||||||
|
<Group>
|
||||||
|
<TextTitle isNoSelect order={3}>
|
||||||
|
{t('page.favorites.title', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</TextTitle>
|
||||||
|
<Icon icon="dropdown" size="xl" />
|
||||||
|
</Group>
|
||||||
|
<Text isMuted size="sm">
|
||||||
|
{itemType === LibraryItem.ALBUM &&
|
||||||
|
t('entity.album_other', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
{itemType === LibraryItem.ALBUM_ARTIST &&
|
||||||
|
t('entity.artist_other', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
{itemType === LibraryItem.SONG &&
|
||||||
|
t('entity.track_other', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</DropdownMenu.Target>
|
||||||
|
<DropdownMenu.Dropdown>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
isSelected={itemType === LibraryItem.SONG}
|
||||||
|
leftSection={<Icon icon="track" size="xl" />}
|
||||||
|
onClick={() => handleItemTypeChange(LibraryItem.SONG)}
|
||||||
|
>
|
||||||
|
{t('entity.track_other', { postProcess: 'sentenceCase' })}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
isSelected={itemType === LibraryItem.ALBUM}
|
||||||
|
leftSection={<Icon icon="album" size="xl" />}
|
||||||
|
onClick={() => handleItemTypeChange(LibraryItem.ALBUM)}
|
||||||
|
>
|
||||||
|
{t('entity.album_other', { postProcess: 'sentenceCase' })}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
isSelected={itemType === LibraryItem.ALBUM_ARTIST}
|
||||||
|
leftSection={<Icon icon="artist" size="xl" />}
|
||||||
|
onClick={() =>
|
||||||
|
handleItemTypeChange(LibraryItem.ALBUM_ARTIST)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('entity.artist_other', { postProcess: 'sentenceCase' })}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</DropdownMenu.Dropdown>
|
||||||
|
</DropdownMenu>
|
||||||
|
</LibraryHeaderBar.Title>
|
||||||
|
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
||||||
|
{itemCount}
|
||||||
|
</LibraryHeaderBar.Badge>
|
||||||
|
</LibraryHeaderBar>
|
||||||
|
<Group>
|
||||||
|
<ListSearchInput />
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
</PageHeader>
|
||||||
|
<FilterBar>
|
||||||
|
{itemType === LibraryItem.ALBUM && <AlbumListHeaderFilters />}
|
||||||
|
{itemType === LibraryItem.ALBUM_ARTIST && <AlbumArtistListHeaderFilters />}
|
||||||
|
{itemType === LibraryItem.SONG && <SongListHeaderFilters />}
|
||||||
|
</FilterBar>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PlayButton = ({ itemType, query }: { itemType: LibraryItem; query: Record<string, any> }) => {
|
||||||
|
return <LibraryHeaderBar.PlayButton itemType={itemType} listQuery={query} variant="filled" />;
|
||||||
|
};
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { useSearchParams } from 'react-router';
|
||||||
|
|
||||||
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
|
import { FavoritesContent } from '/@/renderer/features/favorites/components/favorites-content';
|
||||||
|
import { FavoritesHeader } from '/@/renderer/features/favorites/components/favorites-header';
|
||||||
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
|
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
const FavoritesRoute = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const itemType = (searchParams.get('type') as LibraryItem) || LibraryItem.SONG;
|
||||||
|
|
||||||
|
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const getPageKey = (type: LibraryItem): ItemListKey => {
|
||||||
|
switch (type) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
|
return ItemListKey.ALBUM;
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return ItemListKey.ALBUM_ARTIST;
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return ItemListKey.SONG;
|
||||||
|
default:
|
||||||
|
return ItemListKey.SONG;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const pageKey = useMemo(() => getPageKey(itemType), [itemType]);
|
||||||
|
|
||||||
|
const customFilters = useMemo(() => {
|
||||||
|
switch (itemType) {
|
||||||
|
case LibraryItem.ALBUM:
|
||||||
|
return { favorite: true };
|
||||||
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
|
return { favorite: true };
|
||||||
|
case LibraryItem.SONG:
|
||||||
|
return { favorite: true };
|
||||||
|
default:
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}, [itemType]);
|
||||||
|
|
||||||
|
const providerValue = useMemo(() => {
|
||||||
|
return {
|
||||||
|
customFilters,
|
||||||
|
itemCount,
|
||||||
|
pageKey,
|
||||||
|
setItemCount,
|
||||||
|
};
|
||||||
|
}, [customFilters, itemCount, pageKey]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedPage>
|
||||||
|
<ListContext.Provider value={providerValue}>
|
||||||
|
<FavoritesHeader itemType={itemType} />
|
||||||
|
<FavoritesContent itemType={itemType} />
|
||||||
|
</ListContext.Provider>
|
||||||
|
</AnimatedPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FavoritesRouteWithBoundary = () => {
|
||||||
|
return (
|
||||||
|
<PageErrorBoundary>
|
||||||
|
<FavoritesRoute />
|
||||||
|
</PageErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default FavoritesRouteWithBoundary;
|
||||||
@@ -10,6 +10,7 @@ const SIDEBAR_ITEMS: Array<[string, string]> = [
|
|||||||
['Now Playing', 'page.sidebar.nowPlaying'],
|
['Now Playing', 'page.sidebar.nowPlaying'],
|
||||||
['Playlists', 'page.sidebar.playlists'],
|
['Playlists', 'page.sidebar.playlists'],
|
||||||
['Search', 'page.sidebar.search'],
|
['Search', 'page.sidebar.search'],
|
||||||
|
['Favorites', 'page.sidebar.favorites'],
|
||||||
['Settings', 'page.sidebar.settings'],
|
['Settings', 'page.sidebar.settings'],
|
||||||
['Tracks', 'page.sidebar.tracks'],
|
['Tracks', 'page.sidebar.tracks'],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export const CollapsedSidebar = () => {
|
|||||||
'\n',
|
'\n',
|
||||||
),
|
),
|
||||||
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
||||||
|
Favorites: t('page.sidebar.favorites', { postProcess: 'titleCase' }),
|
||||||
Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }),
|
Folders: t('page.sidebar.folders', { postProcess: 'titleCase' }),
|
||||||
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
||||||
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const MobileSidebar = () => {
|
|||||||
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
||||||
Artists: t('page.sidebar.albumArtists', { postProcess: 'titleCase' }),
|
Artists: t('page.sidebar.albumArtists', { postProcess: 'titleCase' }),
|
||||||
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
||||||
|
Favorites: t('page.sidebar.favorites', { postProcess: 'titleCase' }),
|
||||||
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
||||||
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
||||||
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
|
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
|
||||||
@@ -105,7 +106,7 @@ export const MobileSidebar = () => {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
<div className={styles.serverSelectorWrapper}>
|
<div className={styles.serverSelectorWrapper}>
|
||||||
<ServerSelector showImage={false} />
|
<ServerSelector />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
RiFlag2Line,
|
RiFlag2Line,
|
||||||
RiFolder3Fill,
|
RiFolder3Fill,
|
||||||
RiFolder3Line,
|
RiFolder3Line,
|
||||||
|
RiHeartFill,
|
||||||
|
RiHeartLine,
|
||||||
RiHome6Fill,
|
RiHome6Fill,
|
||||||
RiHome6Line,
|
RiHome6Line,
|
||||||
RiMusic2Fill,
|
RiMusic2Fill,
|
||||||
@@ -67,6 +69,10 @@ export const SidebarIcon = ({ active, route, size }: SidebarIconProps) => {
|
|||||||
if (active) return <RiSearchFill size={size} />;
|
if (active) return <RiSearchFill size={size} />;
|
||||||
return <RiSearchLine size={size} />;
|
return <RiSearchLine size={size} />;
|
||||||
default:
|
default:
|
||||||
|
if (route.startsWith(AppRoute.FAVORITES)) {
|
||||||
|
if (active) return <RiHeartFill size={size} />;
|
||||||
|
return <RiHeartLine size={size} />;
|
||||||
|
}
|
||||||
return <RiHome6Line size={size} />;
|
return <RiHome6Line size={size} />;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export const Sidebar = () => {
|
|||||||
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
Albums: t('page.sidebar.albums', { postProcess: 'titleCase' }),
|
||||||
Artists: t('page.sidebar.albumArtists', { postProcess: 'titleCase' }),
|
Artists: t('page.sidebar.albumArtists', { postProcess: 'titleCase' }),
|
||||||
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
'Artists-all': t('page.sidebar.artists', { postProcess: 'titleCase' }),
|
||||||
|
Favorites: t('page.sidebar.favorites', { postProcess: 'titleCase' }),
|
||||||
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
Genres: t('page.sidebar.genres', { postProcess: 'titleCase' }),
|
||||||
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
Home: t('page.sidebar.home', { postProcess: 'titleCase' }),
|
||||||
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
|
'Now Playing': t('page.sidebar.nowPlaying', { postProcess: 'titleCase' }),
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ const GenreDetailRoute = lazy(
|
|||||||
|
|
||||||
const SearchRoute = lazy(() => import('/@/renderer/features/search/routes/search-route'));
|
const SearchRoute = lazy(() => import('/@/renderer/features/search/routes/search-route'));
|
||||||
|
|
||||||
|
const FavoritesRoute = lazy(() => import('/@/renderer/features/favorites/routes/favorites-route'));
|
||||||
|
|
||||||
export const AppRouter = () => {
|
export const AppRouter = () => {
|
||||||
const router = (
|
const router = (
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
@@ -90,6 +92,7 @@ export const AppRouter = () => {
|
|||||||
<Route element={<HomeRoute />} index />
|
<Route element={<HomeRoute />} index />
|
||||||
<Route element={<HomeRoute />} path={AppRoute.HOME} />
|
<Route element={<HomeRoute />} path={AppRoute.HOME} />
|
||||||
<Route element={<SearchRoute />} path={AppRoute.SEARCH} />
|
<Route element={<SearchRoute />} path={AppRoute.SEARCH} />
|
||||||
|
<Route element={<FavoritesRoute />} path={AppRoute.FAVORITES} />
|
||||||
<Route
|
<Route
|
||||||
element={<NowPlayingRoute />}
|
element={<NowPlayingRoute />}
|
||||||
path={AppRoute.NOW_PLAYING}
|
path={AppRoute.NOW_PLAYING}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export enum AppRoute {
|
|||||||
ACTION_REQUIRED = '/action-required',
|
ACTION_REQUIRED = '/action-required',
|
||||||
EXPLORE = '/explore',
|
EXPLORE = '/explore',
|
||||||
FAKE_LIBRARY_ALBUM_DETAILS = '/library/albums/dummy/:albumId',
|
FAKE_LIBRARY_ALBUM_DETAILS = '/library/albums/dummy/:albumId',
|
||||||
|
FAVORITES = '/favorites',
|
||||||
HOME = '/',
|
HOME = '/',
|
||||||
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
LIBRARY_ALBUM_ARTISTS = '/library/album-artists',
|
||||||
LIBRARY_ALBUM_ARTISTS_DETAIL = '/library/album-artists/:albumArtistId',
|
LIBRARY_ALBUM_ARTISTS_DETAIL = '/library/album-artists/:albumArtistId',
|
||||||
|
|||||||
@@ -527,6 +527,12 @@ export const sidebarItems: SidebarItemType[] = [
|
|||||||
route: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
|
route: generatePath(AppRoute.SEARCH, { itemType: LibraryItem.SONG }),
|
||||||
},
|
},
|
||||||
{ disabled: false, id: 'Home', label: i18n.t('page.sidebar.home'), route: AppRoute.HOME },
|
{ disabled: false, id: 'Home', label: i18n.t('page.sidebar.home'), route: AppRoute.HOME },
|
||||||
|
{
|
||||||
|
disabled: false,
|
||||||
|
id: 'Favorites',
|
||||||
|
label: i18n.t('page.sidebar.favorites'),
|
||||||
|
route: AppRoute.FAVORITES,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
disabled: false,
|
disabled: false,
|
||||||
id: 'Albums',
|
id: 'Albums',
|
||||||
@@ -1291,10 +1297,19 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version <= 10) {
|
||||||
|
state.general.sidebarItems.push({
|
||||||
|
disabled: false,
|
||||||
|
id: 'Favorites',
|
||||||
|
label: i18n.t('page.sidebar.favorites'),
|
||||||
|
route: AppRoute.FAVORITES,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 10,
|
version: 11,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -210,7 +210,6 @@ const normalizeSong = (
|
|||||||
releaseYear: item.year || null,
|
releaseYear: item.year || null,
|
||||||
sampleRate: item.sampleRate || null,
|
sampleRate: item.sampleRate || null,
|
||||||
size: item.size,
|
size: item.size,
|
||||||
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`,
|
|
||||||
tags: item.tags || null,
|
tags: item.tags || null,
|
||||||
trackNumber: item.trackNumber,
|
trackNumber: item.trackNumber,
|
||||||
updatedAt: item.updatedAt,
|
updatedAt: item.updatedAt,
|
||||||
|
|||||||
@@ -195,7 +195,7 @@ const normalizeSong = (
|
|||||||
tags: null,
|
tags: null,
|
||||||
trackNumber: item.track || 1,
|
trackNumber: item.track || 1,
|
||||||
updatedAt: '',
|
updatedAt: '',
|
||||||
userFavorite: item.starred || false,
|
userFavorite: Boolean(item.starred) || false,
|
||||||
userRating: item.userRating || null,
|
userRating: item.userRating || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -232,7 +232,7 @@ const normalizeAlbumArtist = (
|
|||||||
playCount: null,
|
playCount: null,
|
||||||
similarArtists: [],
|
similarArtists: [],
|
||||||
songCount: null,
|
songCount: null,
|
||||||
userFavorite: false,
|
userFavorite: Boolean(item.starred) || false,
|
||||||
userRating: null,
|
userRating: null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -290,7 +290,7 @@ const normalizeAlbum = (
|
|||||||
) || [],
|
) || [],
|
||||||
tags: null,
|
tags: null,
|
||||||
updatedAt: item.created,
|
updatedAt: item.created,
|
||||||
userFavorite: item.starred || false,
|
userFavorite: Boolean(item.starred) || false,
|
||||||
userRating: item.userRating || null,
|
userRating: item.userRating || null,
|
||||||
version: item.version || null,
|
version: item.version || null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -661,6 +661,7 @@ export type AlbumArtistListCountArgs = BaseEndpointArgs & {
|
|||||||
|
|
||||||
export interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
|
export interface AlbumArtistListQuery extends BaseQuery<AlbumArtistListSort> {
|
||||||
_custom?: Record<string, any>;
|
_custom?: Record<string, any>;
|
||||||
|
favorite?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
musicFolderId?: string | string[];
|
musicFolderId?: string | string[];
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
@@ -753,6 +754,7 @@ export type ArtistListCountArgs = BaseEndpointArgs & { query: ListCountQuery<Art
|
|||||||
|
|
||||||
export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
|
export interface ArtistListQuery extends BaseQuery<ArtistListSort> {
|
||||||
_custom?: Record<string, any>;
|
_custom?: Record<string, any>;
|
||||||
|
favorite?: boolean;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
musicFolderId?: string | string[];
|
musicFolderId?: string | string[];
|
||||||
role?: string;
|
role?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user