[enhancement]: Support toggling Album/Track view for gneres (#591)

* [enhancement]: Support toggling Album/Track view for gneres

The _primary_ purpose of this PR is to enable viewing tracks OR albums for genres.
This has a few requirements:
1. Ability to set default route for genres, **except** when already on song/album page
2. Ability to toggle between album and genre view
3. Fixed refresh for genre ID

Additionally, there was some refactoring:
- Since the *-list-headers had very similar functions for search, export that as a hook instead

* also use hook for album artist

* support switching albumartist tracks/albums

* remove toggle on song/album list, simplify logic
This commit is contained in:
Kendall Garner 2024-04-20 06:14:31 +00:00 committed by GitHub
parent 595eba152a
commit ba531505af
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 336 additions and 158 deletions

View file

@ -282,6 +282,8 @@
"moreFromGeneric": "more from {{item}}" "moreFromGeneric": "more from {{item}}"
}, },
"albumList": { "albumList": {
"artistAlbums": "Albums by {{artist}}",
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"title": "$t(entity.album_other)" "title": "$t(entity.album_other)"
}, },
"appMenu": { "appMenu": {
@ -336,6 +338,8 @@
"upNext": "up next" "upNext": "up next"
}, },
"genreList": { "genreList": {
"showAlbums": "show $t(entity.genre_one) $t(entity.album_other)",
"showTracks": "show $t(entity.genre_one) $t(entity.track_other)",
"title": "$t(entity.genre_other)" "title": "$t(entity.genre_other)"
}, },
"globalSearch": { "globalSearch": {
@ -474,6 +478,8 @@
"gaplessAudio": "gapless audio", "gaplessAudio": "gapless audio",
"gaplessAudio_description": "sets the gapless audio setting for mpv", "gaplessAudio_description": "sets the gapless audio setting for mpv",
"gaplessAudio_optionWeak": "weak (recommended)", "gaplessAudio_optionWeak": "weak (recommended)",
"genreBehavior": "genre page default behavior",
"genreBehavior_description": "determines whether clicking on a genre opens by default in track or album list",
"globalMediaHotkeys": "global media hotkeys", "globalMediaHotkeys": "global media hotkeys",
"globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback", "globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback",
"homeConfiguration": "home page configuration", "homeConfiguration": "home page configuration",

View file

@ -4,10 +4,11 @@ import { generatePath, Link } from 'react-router-dom';
import type { AlbumArtist, Artist } from '/@/renderer/api/types'; import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components/text'; import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell'; import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { AppRoute } from '/@/renderer/router/routes';
import { Separator } from '/@/renderer/components/separator'; import { Separator } from '/@/renderer/components/separator';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
export const GenreCell = ({ value, data }: ICellRendererParams) => { export const GenreCell = ({ value, data }: ICellRendererParams) => {
const genrePath = useGenreRoute();
return ( return (
<CellContainer $position="left"> <CellContainer $position="left">
<Text <Text
@ -24,9 +25,7 @@ export const GenreCell = ({ value, data }: ICellRendererParams) => {
component={Link} component={Link}
overflow="hidden" overflow="hidden"
size="md" size="md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, { to={generatePath(genrePath, { genreId: item.id })}
genreId: item.id,
})}
> >
{item.name || '—'} {item.name || '—'}
</Text> </Text>

View file

@ -45,6 +45,7 @@ import {
} from '/@/renderer/store/settings.store'; } from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types'; import { Play } from '/@/renderer/types';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify'; import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
const isFullWidthRow = (node: RowNode) => { const isFullWidthRow = (node: RowNode) => {
return node.id?.startsWith('disc-'); return node.id?.startsWith('disc-');
@ -81,6 +82,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
const isFocused = useAppFocus(); const isFocused = useAppFocus();
const currentSong = useCurrentSong(); const currentSong = useCurrentSong();
const { externalLinks } = useGeneralSettings(); const { externalLinks } = useGeneralSettings();
const genreRoute = useGenreRoute();
const columnDefs = useMemo( const columnDefs = useMemo(
() => getColumnDefs(tableConfig.columns, false, 'albumDetail'), () => getColumnDefs(tableConfig.columns, false, 'albumDetail'),
@ -389,7 +391,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
component={Link} component={Link}
radius={0} radius={0}
size="md" size="md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, { to={generatePath(genreRoute, {
genreId: genre.id, genreId: genre.id,
})} })}
variant="outline" variant="outline"

View file

@ -1,63 +1,59 @@
import type { ChangeEvent, MutableRefObject } from 'react'; import { useEffect, useRef, type ChangeEvent, type 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 { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components'; import { PageHeader, SearchInput } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters'; import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { import { AlbumListFilter, useCurrentServer, usePlayButtonBehavior } from '/@/renderer/store';
AlbumListFilter,
useCurrentServer,
useListStoreActions,
useListStoreByKey,
usePlayButtonBehavior,
} from '/@/renderer/store';
import { ListDisplayType } from '/@/renderer/types';
import { titleCase } from '/@/renderer/utils'; import { titleCase } from '/@/renderer/utils';
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
interface AlbumListHeaderProps { interface AlbumListHeaderProps {
genreId?: string;
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number; itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
title?: string; title?: string;
} }
export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumListHeaderProps) => { export const AlbumListHeader = ({
genreId,
itemCount,
gridRef,
tableRef,
title,
}: AlbumListHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const { setFilter, setTablePagination } = useListStoreActions();
const cq = useContainerQuery(); const cq = useContainerQuery();
const { pageKey, handlePlay } = useListContext();
const { display, filter } = useListStoreByKey({ key: pageKey });
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
const genreRef = useRef<string>();
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ const { filter, handlePlay, refresh, search } = useDisplayRefresh({
gridRef,
itemType: LibraryItem.ALBUM, itemType: LibraryItem.ALBUM,
server, server,
tableRef,
}); });
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => { const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value === '' ? undefined : e.target.value; const updatedFilters = search(e) as AlbumListFilter;
const updatedFilters = setFilter({
data: { searchTerm },
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { refresh(updatedFilters);
handleRefreshTable(tableRef, updatedFilters);
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else {
handleRefreshGrid(gridRef, updatedFilters);
}
}, 500); }, 500);
useEffect(() => {
if (genreRef.current && genreRef.current !== genreId) {
refresh(filter);
}
genreRef.current = genreId;
}, [filter, genreId, refresh, tableRef]);
return ( return (
<Stack <Stack
ref={cq.ref} ref={cq.ref}

View file

@ -1,10 +1,11 @@
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 { api } from '/@/renderer/api'; import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { LibraryItem } from '/@/renderer/api/types'; import { GenreListSort, LibraryItem, SortOrder } from '/@/renderer/api/types';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content'; import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
@ -15,19 +16,32 @@ import { AnimatedPage } from '/@/renderer/features/shared';
import { queryClient } from '/@/renderer/lib/react-query'; import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store'; import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
import { Play } from '/@/renderer/types'; import { Play } from '/@/renderer/types';
import { useGenreList } from '/@/renderer/features/genres';
import { titleCase } from '/@/renderer/utils';
const AlbumListRoute = () => { const AlbumListRoute = () => {
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();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { albumArtistId } = useParams(); const { albumArtistId, genreId } = useParams();
const pageKey = albumArtistId ? `albumArtistAlbum` : 'album'; const pageKey = albumArtistId ? `albumArtistAlbum` : 'album';
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const customFilters = useMemo(() => { const customFilters = useMemo(() => {
const value = { const value = {
...(albumArtistId && { artistIds: [albumArtistId] }), ...(albumArtistId && { artistIds: [albumArtistId] }),
...(genreId && {
_custom: {
jellyfin: {
GenreIds: genreId,
},
navidrome: {
genre_id: genreId,
},
},
}),
}; };
if (isEmpty(value)) { if (isEmpty(value)) {
@ -35,13 +49,35 @@ const AlbumListRoute = () => {
} }
return value; return value;
}, [albumArtistId]); }, [albumArtistId, genreId]);
const albumListFilter = useListFilterByKey({ const albumListFilter = useListFilterByKey({
filter: customFilters, filter: customFilters,
key: pageKey, key: pageKey,
}); });
const genreList = useGenreList({
options: {
cacheTime: 1000 * 60 * 60,
enabled: !!genreId,
},
query: {
sortBy: GenreListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
serverId: server?.id,
});
const genreTitle = useMemo(() => {
if (!genreList.data) return '';
const genre = genreList.data.items.find((g) => g.id === genreId);
if (!genre) return 'Unknown';
return genre?.name;
}, [genreId, genreList.data]);
const itemCountCheck = useAlbumList({ const itemCountCheck = useAlbumList({
options: { options: {
cacheTime: 1000 * 60, cacheTime: 1000 * 60,
@ -98,19 +134,27 @@ const AlbumListRoute = () => {
return { return {
customFilters, customFilters,
handlePlay, handlePlay,
id: albumArtistId ?? undefined, id: albumArtistId ?? genreId,
pageKey, pageKey,
}; };
}, [albumArtistId, customFilters, handlePlay, pageKey]); }, [albumArtistId, customFilters, genreId, handlePlay, pageKey]);
const artist = searchParams.get('artistName');
const title = artist
? t('page.albumList.artistAlbums', { artist })
: genreId
? t('page.albumList.genreAlbums', { genre: titleCase(genreTitle) })
: undefined;
return ( return (
<AnimatedPage> <AnimatedPage>
<ListContext.Provider value={providerValue}> <ListContext.Provider value={providerValue}>
<AlbumListHeader <AlbumListHeader
genreId={genreId}
gridRef={gridRef} gridRef={gridRef}
itemCount={itemCount} itemCount={itemCount}
tableRef={tableRef} tableRef={tableRef}
title={searchParams.get('artistName') || undefined} title={title}
/> />
<AlbumListContent <AlbumListContent
gridRef={gridRef} gridRef={gridRef}

View file

@ -40,6 +40,7 @@ import { useCurrentServer } from '/@/renderer/store';
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { CardRow, Play, TableColumn } from '/@/renderer/types'; import { CardRow, Play, TableColumn } from '/@/renderer/types';
import { sanitize } from '/@/renderer/utils/sanitize'; import { sanitize } from '/@/renderer/utils/sanitize';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
const ContentContainer = styled.div` const ContentContainer = styled.div`
position: relative; position: relative;
@ -69,6 +70,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
const cq = useContainerQuery(); const cq = useContainerQuery();
const handlePlayQueueAdd = usePlayQueueAdd(); const handlePlayQueueAdd = usePlayQueueAdd();
const server = useCurrentServer(); const server = useCurrentServer();
const genrePath = useGenreRoute();
const detailQuery = useAlbumArtistDetail({ const detailQuery = useAlbumArtistDetail({
query: { id: albumArtistId }, query: { id: albumArtistId },
@ -414,7 +416,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
component={Link} component={Link}
radius="md" radius="md"
size="md" size="md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, { to={generatePath(genrePath, {
genreId: genre.id, genreId: genre.id,
})} })}
variant="outline" variant="outline"

View file

@ -3,8 +3,6 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
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 { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useListContext } from '../../../context/list-context';
import { useListStoreByKey } from '../../../store/list.store';
import { FilterBar } from '../../shared/components/filter-bar'; import { FilterBar } from '../../shared/components/filter-bar';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components'; import { PageHeader, SearchInput } from '/@/renderer/components';
@ -12,9 +10,8 @@ import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters'; import { AlbumArtistListHeaderFilters } from '/@/renderer/features/artists/components/album-artist-list-header-filters';
import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; import { AlbumArtistListFilter, useCurrentServer } from '/@/renderer/store';
import { AlbumArtistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store'; import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
import { ListDisplayType } from '/@/renderer/types';
interface AlbumArtistListHeaderProps { interface AlbumArtistListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
@ -29,30 +26,18 @@ export const AlbumArtistListHeader = ({
}: AlbumArtistListHeaderProps) => { }: AlbumArtistListHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const { pageKey } = useListContext();
const { display, filter } = useListStoreByKey({ key: pageKey });
const { setFilter, setTablePagination } = useListStoreActions();
const cq = useContainerQuery(); const cq = useContainerQuery();
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ const { filter, refresh, search } = useDisplayRefresh({
gridRef,
itemType: LibraryItem.ALBUM_ARTIST, itemType: LibraryItem.ALBUM_ARTIST,
server, server,
tableRef,
}); });
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => { const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value === '' ? undefined : e.target.value; const updatedFilters = search(e) as AlbumArtistListFilter;
const updatedFilters = setFilter({ refresh(updatedFilters);
data: { searchTerm },
itemType: LibraryItem.ALBUM_ARTIST,
key: pageKey,
}) as AlbumArtistListFilter;
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
handleRefreshTable(tableRef, updatedFilters);
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else {
handleRefreshGrid(gridRef, updatedFilters);
}
}, 500); }, 500);
return ( return (

View file

@ -13,9 +13,9 @@ import {
} from '/@/renderer/components/virtual-grid'; } from '/@/renderer/components/virtual-grid';
import { useListContext } from '/@/renderer/context/list-context'; import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player'; import { usePlayQueueAdd } from '/@/renderer/features/player';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store'; import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import { CardRow, ListDisplayType } from '/@/renderer/types'; import { CardRow, ListDisplayType } from '/@/renderer/types';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
export const GenreListGridView = ({ gridRef, itemCount }: any) => { export const GenreListGridView = ({ gridRef, itemCount }: any) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -24,6 +24,7 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
const { pageKey, id } = useListContext(); const { pageKey, id } = useListContext();
const { grid, display, filter } = useListStoreByKey({ key: pageKey }); const { grid, display, filter } = useListStoreByKey({ key: pageKey });
const { setGrid } = useListStoreActions(); const { setGrid } = useListStoreActions();
const genrePath = useGenreRoute();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const scrollOffset = searchParams.get('scrollOffset'); const scrollOffset = searchParams.get('scrollOffset');
@ -137,7 +138,7 @@ export const GenreListGridView = ({ gridRef, itemCount }: any) => {
loading={itemCount === undefined || itemCount === null} loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40} minimumBatchSize={40}
route={{ route={{
route: AppRoute.LIBRARY_GENRES_SONGS, route: genrePath,
slugs: [{ idProperty: 'id', slugProperty: 'genreId' }], slugs: [{ idProperty: 'id', slugProperty: 'genreId' }],
}} }}
width={width} width={width}

View file

@ -3,7 +3,14 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { Divider, Flex, Group, Stack } from '@mantine/core'; import { Divider, Flex, Group, Stack } from '@mantine/core';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiFolder2Fill, RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri'; import {
RiAlbumLine,
RiFolder2Fill,
RiMoreFill,
RiMusic2Line,
RiRefreshLine,
RiSettings3Fill,
} from 'react-icons/ri';
import { queryKeys } from '/@/renderer/api/query-keys'; import { queryKeys } from '/@/renderer/api/query-keys';
import { GenreListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types'; import { GenreListSort, LibraryItem, ServerType, SortOrder } from '/@/renderer/api/types';
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components'; import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
@ -15,9 +22,12 @@ import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
import { import {
GenreListFilter, GenreListFilter,
GenreTarget,
useCurrentServer, useCurrentServer,
useGeneralSettings,
useListStoreActions, useListStoreActions,
useListStoreByKey, useListStoreByKey,
useSettingsStoreActions,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { ListDisplayType, TableColumn } from '/@/renderer/types'; import { ListDisplayType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n'; import i18n from '/@/i18n/i18n';
@ -52,6 +62,8 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions(); const { setFilter, setTable, setGrid, setDisplayType } = useListStoreActions();
const { display, filter, table, grid } = useListStoreByKey({ key: pageKey }); const { display, filter, table, grid } = useListStoreByKey({ key: pageKey });
const cq = useContainerQuery(); const cq = useContainerQuery();
const { genreTarget } = useGeneralSettings();
const { setGenreBehavior } = useSettingsStoreActions();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({ const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({
itemType: LibraryItem.GENRE, itemType: LibraryItem.GENRE,
@ -208,6 +220,11 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
return filter.musicFolderId !== undefined; return filter.musicFolderId !== undefined;
}, [filter.musicFolderId]); }, [filter.musicFolderId]);
const handleGenreToggle = useCallback(() => {
const newState = genreTarget === GenreTarget.ALBUM ? GenreTarget.TRACK : GenreTarget.ALBUM;
setGenreBehavior(newState);
}, [genreTarget, setGenreBehavior]);
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
<Group <Group
@ -309,6 +326,23 @@ export const GenreListHeaderFilters = ({ gridRef, tableRef }: GenreListHeaderFil
{t('common.refresh', { postProcess: 'titleCase' })} {t('common.refresh', { postProcess: 'titleCase' })}
</DropdownMenu.Item> </DropdownMenu.Item>
</DropdownMenu.Dropdown> </DropdownMenu.Dropdown>
<Divider orientation="vertical" />
<Button
compact
size="md"
tooltip={{
label: t(
genreTarget === GenreTarget.ALBUM
? 'page.genreList.showAlbums'
: 'page.genreList.showTracks',
{ postProcess: 'sentenceCase' },
),
}}
variant="subtle"
onClick={handleGenreToggle}
>
{genreTarget === GenreTarget.ALBUM ? <RiAlbumLine /> : <RiMusic2Line />}
</Button>
</DropdownMenu> </DropdownMenu>
</Group> </Group>
<Group <Group

View file

@ -5,19 +5,12 @@ import debounce from 'lodash/debounce';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components'; import { PageHeader, SearchInput } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters'; import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; import { GenreListFilter, useCurrentServer } from '/@/renderer/store';
import {
GenreListFilter,
useCurrentServer,
useListStoreActions,
useListStoreByKey,
} from '/@/renderer/store';
import { ListDisplayType } from '/@/renderer/types';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
interface GenreListHeaderProps { interface GenreListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
@ -29,34 +22,18 @@ export const GenreListHeader = ({ itemCount, gridRef, tableRef }: GenreListHeade
const { t } = useTranslation(); const { t } = useTranslation();
const cq = useContainerQuery(); const cq = useContainerQuery();
const server = useCurrentServer(); const server = useCurrentServer();
const { pageKey } = useListContext(); const { filter, refresh, search } = useDisplayRefresh({
const { display, filter } = useListStoreByKey({ key: pageKey }); gridRef,
const { setFilter, setTablePagination } = useListStoreActions();
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
itemType: LibraryItem.GENRE, itemType: LibraryItem.GENRE,
server, server,
tableRef,
}); });
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => { const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value === '' ? undefined : e.target.value; const updatedFilters = search(e) as GenreListFilter;
const updatedFilters = setFilter({ refresh(updatedFilters);
data: { searchTerm },
itemType: LibraryItem.GENRE,
key: pageKey,
}) as GenreListFilter;
const filterWithCustom = {
...updatedFilters,
};
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
handleRefreshTable(tableRef, filterWithCustom);
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else {
handleRefreshGrid(gridRef, filterWithCustom);
}
}, 500); }, 500);
return ( return (
<Stack <Stack
ref={cq.ref} ref={cq.ref}

View file

@ -9,7 +9,7 @@ import { useCurrentServer } from '/@/renderer/store';
import { MutableRefObject, useCallback } from 'react'; import { MutableRefObject, useCallback } from 'react';
import { RowDoubleClickedEvent } from '@ag-grid-community/core'; import { RowDoubleClickedEvent } from '@ag-grid-community/core';
import { generatePath, useNavigate } from 'react-router'; import { generatePath, useNavigate } from 'react-router';
import { AppRoute } from '/@/renderer/router/routes'; import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
interface GenreListTableViewProps { interface GenreListTableViewProps {
itemCount?: number; itemCount?: number;
@ -20,6 +20,7 @@ export const GenreListTableView = ({ tableRef, itemCount }: GenreListTableViewPr
const server = useCurrentServer(); const server = useCurrentServer();
const { pageKey, customFilters } = useListContext(); const { pageKey, customFilters } = useListContext();
const navigate = useNavigate(); const navigate = useNavigate();
const genrePath = useGenreRoute();
const tableProps = useVirtualTable({ const tableProps = useVirtualTable({
contextMenu: GENRE_CONTEXT_MENU_ITEMS, contextMenu: GENRE_CONTEXT_MENU_ITEMS,
@ -36,9 +37,9 @@ export const GenreListTableView = ({ tableRef, itemCount }: GenreListTableViewPr
const { data } = e; const { data } = e;
if (!data) return; if (!data) return;
navigate(generatePath(AppRoute.LIBRARY_GENRES_SONGS, { genreId: data.id })); navigate(generatePath(genrePath, { genreId: data.id }));
}, },
[navigate], [genrePath, navigate],
); );
return ( return (

View file

@ -14,6 +14,7 @@ import { generatePath } from 'react-router';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { Separator } from '/@/renderer/components/separator'; import { Separator } from '/@/renderer/components/separator';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
export type ItemDetailsModalProps = { export type ItemDetailsModalProps = {
item: Album | AlbumArtist | Song; item: Album | AlbumArtist | Song;
@ -83,8 +84,10 @@ const formatComment = (item: Album | Song) =>
const formatDate = (key: string | null) => (key ? dayjs(key).fromNow() : ''); const formatDate = (key: string | null) => (key ? dayjs(key).fromNow() : '');
const formatGenre = (item: Album | AlbumArtist | Song) => const FormatGenre = (item: Album | AlbumArtist | Song) => {
item.genres?.map((genre, index) => ( const genreRoute = useGenreRoute();
return item.genres?.map((genre, index) => (
<span key={genre.id}> <span key={genre.id}>
{index > 0 && <Separator />} {index > 0 && <Separator />}
<Text <Text
@ -92,19 +95,14 @@ const formatGenre = (item: Album | AlbumArtist | Song) =>
component={Link} component={Link}
overflow="visible" overflow="visible"
size="md" size="md"
to={ to={genre.id ? generatePath(genreRoute, { genreId: genre.id }) : ''}
genre.id
? generatePath(AppRoute.LIBRARY_GENRES_SONGS, {
genreId: genre.id,
})
: ''
}
weight={500} weight={500}
> >
{genre.name || '—'} {genre.name || '—'}
</Text> </Text>
</span> </span>
)); ));
};
const formatRating = (item: Album | AlbumArtist | Song) => const formatRating = (item: Album | AlbumArtist | Song) =>
item.userRating !== null ? ( item.userRating !== null ? (
@ -120,7 +118,7 @@ const BoolField = (key: boolean) =>
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [ const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
{ key: 'name', label: 'common.title' }, { key: 'name', label: 'common.title' },
{ label: 'entity.albumArtist_one', render: formatArtists(true) }, { label: 'entity.albumArtist_one', render: formatArtists(true) },
{ label: 'entity.genre_other', render: formatGenre }, { label: 'entity.genre_other', render: FormatGenre },
{ {
label: 'common.duration', label: 'common.duration',
render: (album) => album.duration && formatDurationString(album.duration), render: (album) => album.duration && formatDurationString(album.duration),
@ -166,7 +164,7 @@ const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
{ key: 'name', label: 'common.name' }, { key: 'name', label: 'common.name' },
{ label: 'entity.genre_other', render: formatGenre }, { label: 'entity.genre_other', render: FormatGenre },
{ {
label: 'common.duration', label: 'common.duration',
render: (artist) => artist.duration && formatDurationString(artist.duration), render: (artist) => artist.duration && formatDurationString(artist.duration),
@ -240,7 +238,7 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
{ key: 'discNumber', label: 'common.disc' }, { key: 'discNumber', label: 'common.disc' },
{ key: 'trackNumber', label: 'common.trackNumber' }, { key: 'trackNumber', label: 'common.trackNumber' },
{ key: 'releaseYear', label: 'filter.releaseYear' }, { key: 'releaseYear', label: 'filter.releaseYear' },
{ label: 'entity.genre_other', render: formatGenre }, { label: 'entity.genre_other', render: FormatGenre },
{ {
label: 'common.duration', label: 'common.duration',
render: (song) => formatDurationString(song.duration), render: (song) => formatDurationString(song.duration),

View file

@ -8,15 +8,12 @@ import { CreatePlaylistForm } from '/@/renderer/features/playlists/components/cr
import { PlaylistListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-list-header-filters'; import { PlaylistListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-list-header-filters';
import { LibraryHeaderBar } from '/@/renderer/features/shared'; import { LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { PlaylistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store'; import { PlaylistListFilter, useCurrentServer } from '/@/renderer/store';
import { ListDisplayType } from '/@/renderer/types';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiFileAddFill } from 'react-icons/ri'; import { RiFileAddFill } from 'react-icons/ri';
import { LibraryItem, ServerType } from '/@/renderer/api/types'; import { LibraryItem, ServerType } from '/@/renderer/api/types';
import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh'; import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
import { useListContext } from '/@/renderer/context/list-context';
import { useListStoreByKey } from '../../../store/list.store';
interface PlaylistListHeaderProps { interface PlaylistListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
@ -26,11 +23,8 @@ interface PlaylistListHeaderProps {
export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistListHeaderProps) => { export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistListHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { pageKey } = useListContext();
const cq = useContainerQuery(); const cq = useContainerQuery();
const server = useCurrentServer(); const server = useCurrentServer();
const { setFilter, setTablePagination } = useListStoreActions();
const { display, filter } = useListStoreByKey({ key: pageKey });
const handleCreatePlaylistModal = () => { const handleCreatePlaylistModal = () => {
openModal({ openModal({
@ -43,25 +37,16 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
}); });
}; };
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({ const { filter, refresh, search } = useDisplayRefresh({
gridRef,
itemType: LibraryItem.PLAYLIST, itemType: LibraryItem.PLAYLIST,
server, server,
tableRef,
}); });
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => { const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value === '' ? undefined : e.target.value; const updatedFilters = search(e) as PlaylistListFilter;
const updatedFilters = setFilter({ refresh(updatedFilters);
data: { searchTerm },
itemType: LibraryItem.PLAYLIST,
key: pageKey,
}) as PlaylistListFilter;
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
handleRefreshTable(tableRef, updatedFilters);
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else {
handleRefreshGrid(gridRef, updatedFilters);
}
}, 500); }, 500);
return ( return (

View file

@ -4,6 +4,7 @@ import isElectron from 'is-electron';
import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components'; import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components';
import { SettingsSection } from '/@/renderer/features/settings/components/settings-section'; import { SettingsSection } from '/@/renderer/features/settings/components/settings-section';
import { import {
GenreTarget,
SideQueueType, SideQueueType,
useGeneralSettings, useGeneralSettings,
useSettingsStoreActions, useSettingsStoreActions,
@ -341,6 +342,41 @@ export const ControlSettings = () => {
}), }),
title: t('setting.externalLinks', { postProcess: 'sentenceCase' }), title: t('setting.externalLinks', { postProcess: 'sentenceCase' }),
}, },
{
control: (
<Select
data={[
{
label: t('entity.album_other', {
postProcess: 'titleCase',
}),
value: GenreTarget.ALBUM,
},
{
label: t('entity.track_other', {
postProcess: 'titleCase',
}),
value: GenreTarget.TRACK,
},
]}
defaultValue={settings.genreTarget}
onChange={(e) =>
setSettings({
general: {
...settings,
genreTarget: e as GenreTarget,
},
})
}
/>
),
description: t('setting.genreBehavior', {
context: 'description',
postProcess: 'sentenceCase',
}),
isHidden: false,
title: t('setting.genreBehavior', { postProcess: 'sentenceCase' }),
},
]; ];
return <SettingsSection options={controlOptions} />; return <SettingsSection options={controlOptions} />;

View file

@ -1,62 +1,64 @@
import { ChangeEvent, MutableRefObject, useEffect, 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 { 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 { useTranslation } from 'react-i18next';
import { useListStoreByKey } from '../../../store/list.store';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components'; import { PageHeader, SearchInput } from '/@/renderer/components';
import { useListContext } from '/@/renderer/context/list-context';
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared'; import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters'; import { SongListHeaderFilters } from '/@/renderer/features/songs/components/song-list-header-filters';
import { useContainerQuery } from '/@/renderer/hooks'; import { useContainerQuery } from '/@/renderer/hooks';
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh'; import { SongListFilter, useCurrentServer } from '/@/renderer/store';
import { SongListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { ListDisplayType } from '/@/renderer/types';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
interface SongListHeaderProps { interface SongListHeaderProps {
genreId?: string;
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>; gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number; itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>; tableRef: MutableRefObject<AgGridReactType | null>;
title?: string; title?: string;
} }
export const SongListHeader = ({ gridRef, title, itemCount, tableRef }: SongListHeaderProps) => { export const SongListHeader = ({
genreId,
gridRef,
title,
itemCount,
tableRef,
}: SongListHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const server = useCurrentServer(); const server = useCurrentServer();
const { pageKey, handlePlay, customFilters } = useListContext();
const { setFilter, setTablePagination } = useListStoreActions();
const { display, filter } = useListStoreByKey({ key: pageKey });
const cq = useContainerQuery(); const cq = useContainerQuery();
const genreRef = useRef<string>();
const { handleRefreshTable, handleRefreshGrid } = useListFilterRefresh({ const { customFilters, filter, handlePlay, refresh, search } = useDisplayRefresh({
gridRef,
itemType: LibraryItem.SONG, itemType: LibraryItem.SONG,
server, server,
tableRef,
}); });
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => { const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value === '' ? undefined : e.target.value; const updatedFilters = search(e) as SongListFilter;
const updatedFilters = setFilter({
data: { searchTerm },
itemType: LibraryItem.SONG,
key: pageKey,
}) as SongListFilter;
const filterWithCustom = { const filterWithCustom = {
...updatedFilters, ...updatedFilters,
...customFilters, ...customFilters,
}; };
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) { refresh(filterWithCustom);
handleRefreshTable(tableRef, filterWithCustom);
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else {
handleRefreshGrid(gridRef, filterWithCustom);
}
}, 500); }, 500);
useEffect(() => {
if (genreRef.current && genreRef.current !== genreId) {
refresh(customFilters);
}
genreRef.current = genreId;
}, [customFilters, genreId, refresh, tableRef]);
const playButtonBehavior = usePlayButtonBehavior(); const playButtonBehavior = usePlayButtonBehavior();
return ( return (

View file

@ -145,6 +145,7 @@ const TrackListRoute = () => {
<AnimatedPage> <AnimatedPage>
<ListContext.Provider value={providerValue}> <ListContext.Provider value={providerValue}>
<SongListHeader <SongListHeader
genreId={genreId}
gridRef={gridRef} gridRef={gridRef}
itemCount={itemCount} itemCount={itemCount}
tableRef={tableRef} tableRef={tableRef}

View file

@ -0,0 +1,68 @@
import { ChangeEvent, MutableRefObject, useCallback } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import {
UseHandleListFilterChangeProps,
useListFilterRefresh,
} from '/@/renderer/hooks/use-list-filter-refresh';
import { useListContext } from '/@/renderer/context/list-context';
import { ListDisplayType } from '/@/renderer/types';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { useListStoreActions, useListStoreByKey } from '/@/renderer/store';
export type UseDisplayRefreshProps = {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
tableRef: MutableRefObject<AgGridReactType | null>;
} & UseHandleListFilterChangeProps;
export const useDisplayRefresh = ({
isClientSideSort,
gridRef,
itemType,
server,
tableRef,
}: UseDisplayRefreshProps) => {
const { customFilters, pageKey, handlePlay } = useListContext();
const { display, filter } = useListStoreByKey({ key: pageKey });
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
isClientSideSort,
itemType,
server,
});
const { setFilter, setTablePagination } = useListStoreActions();
const refresh = useCallback(
(filter: unknown) => {
if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
handleRefreshTable(tableRef, filter);
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else {
handleRefreshGrid(gridRef, filter);
}
},
[
display,
gridRef,
handleRefreshGrid,
handleRefreshTable,
pageKey,
setTablePagination,
tableRef,
],
);
const search = useCallback(
(e: ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value === '' ? undefined : e.target.value;
const updatedFilters = setFilter({
data: { searchTerm },
itemType,
key: pageKey,
});
return updatedFilters;
},
[itemType, pageKey, setFilter],
);
return { customFilters, filter, handlePlay, refresh, search };
};

View file

@ -0,0 +1,29 @@
import { useLocation } from 'react-router';
import { GenreTarget, useSettingsStore } from '/@/renderer/store';
import { AppRoute } from '/@/renderer/router/routes';
import { useMemo } from 'react';
const ALBUM_REGEX = /albums$/;
const SONG_REGEX = /songs$/;
export const useGenreRoute = () => {
const { pathname } = useLocation();
const matchAlbum = ALBUM_REGEX.test(pathname);
const matchSongs = SONG_REGEX.test(pathname);
const baseState = useSettingsStore((state) =>
state.general.genreTarget === GenreTarget.ALBUM
? AppRoute.LIBRARY_GENRES_ALBUMS
: AppRoute.LIBRARY_GENRES_SONGS,
);
return useMemo(() => {
if (matchAlbum) {
return AppRoute.LIBRARY_GENRES_ALBUMS;
}
if (matchSongs) {
return AppRoute.LIBRARY_GENRES_SONGS;
}
return baseState;
}, [baseState, matchAlbum, matchSongs]);
};

View file

@ -8,7 +8,7 @@ import { BasePaginatedResponse, LibraryItem, ServerListItem } from '/@/renderer/
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid'; import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import orderBy from 'lodash/orderBy'; import orderBy from 'lodash/orderBy';
interface UseHandleListFilterChangeProps { export interface UseHandleListFilterChangeProps {
isClientSideSort?: boolean; isClientSideSort?: boolean;
itemType: LibraryItem; itemType: LibraryItem;
server: ServerListItem | null; server: ServerListItem | null;

View file

@ -171,6 +171,11 @@ export enum BindingActions {
ZOOM_OUT = 'zoomOut', ZOOM_OUT = 'zoomOut',
} }
export enum GenreTarget {
ALBUM = 'album',
TRACK = 'track',
}
export interface SettingsState { export interface SettingsState {
discord: { discord: {
clientId: string; clientId: string;
@ -192,6 +197,7 @@ export interface SettingsState {
defaultFullPlaylist: boolean; defaultFullPlaylist: boolean;
externalLinks: boolean; externalLinks: boolean;
followSystemTheme: boolean; followSystemTheme: boolean;
genreTarget: GenreTarget;
homeItems: SortableItem<HomeItem>[]; homeItems: SortableItem<HomeItem>[];
language: string; language: string;
passwordStore?: string; passwordStore?: string;
@ -311,6 +317,7 @@ const initialState: SettingsState = {
defaultFullPlaylist: true, defaultFullPlaylist: true,
externalLinks: true, externalLinks: true,
followSystemTheme: false, followSystemTheme: false,
genreTarget: GenreTarget.TRACK,
homeItems, homeItems,
language: 'en', language: 'en',
passwordStore: undefined, passwordStore: undefined,
@ -604,6 +611,11 @@ export const useSettingsStore = create<SettingsSlice>()(
state.playback.mpvProperties.audioSampleRateHz = 0; state.playback.mpvProperties.audioSampleRateHz = 0;
}); });
}, },
setGenreBehavior: (target: GenreTarget) => {
set((state) => {
state.general.genreTarget = target;
});
},
setHomeItems: (items: SortableItem<HomeItem>[]) => { setHomeItems: (items: SortableItem<HomeItem>[]) => {
set((state) => { set((state) => {
state.general.homeItems = items; state.general.homeItems = items;