Skip to content

Commit

Permalink
[enhancement]: Support toggling Album/Track view for gneres
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kgarner7 committed Apr 18, 2024
1 parent ebd2f07 commit 602fe05
Show file tree
Hide file tree
Showing 19 changed files with 355 additions and 158 deletions.
5 changes: 5 additions & 0 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,8 @@
"moreFromGeneric": "more from {{item}}"
},
"albumList": {
"genreAlbums": "\"{{genre}}\" $t(entity.album_other)",
"showTracks": "show $t(entity.track_other)",
"title": "$t(entity.album_other)"
},
"appMenu": {
Expand Down Expand Up @@ -384,6 +386,7 @@
"trackList": {
"artistTracks": "Tracks by {{artist}}",
"genreTracks": "\"{{genre}}\" $t(entity.track_other)",
"showAlbums": "show $t(entity.album_other)",
"title": "$t(entity.track_other)"
}
},
Expand Down Expand Up @@ -474,6 +477,8 @@
"gaplessAudio": "gapless audio",
"gaplessAudio_description": "sets the gapless audio setting for mpv",
"gaplessAudio_optionWeak": "weak (recommended)",
"genreBehavior": "genre page default behavior",
"genreBahvior_description": "determines whether clicking on a genre opens by default in track or album list",
"globalMediaHotkeys": "global media hotkeys",
"globalMediaHotkeys_description": "enable or disable the usage of your system media hotkeys to control playback",
"homeConfiguration": "home page configuration",
Expand Down
7 changes: 3 additions & 4 deletions src/renderer/components/virtual-table/cells/genre-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { generatePath, Link } from 'react-router-dom';
import type { AlbumArtist, Artist } from '/@/renderer/api/types';
import { Text } from '/@/renderer/components/text';
import { CellContainer } from '/@/renderer/components/virtual-table/cells/generic-cell';
import { AppRoute } from '/@/renderer/router/routes';
import { Separator } from '/@/renderer/components/separator';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';

export const GenreCell = ({ value, data }: ICellRendererParams) => {
const genrePath = useGenreRoute();
return (
<CellContainer $position="left">
<Text
Expand All @@ -24,9 +25,7 @@ export const GenreCell = ({ value, data }: ICellRendererParams) => {
component={Link}
overflow="hidden"
size="md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, {
genreId: item.id,
})}
to={generatePath(genrePath, { genreId: item.id })}
>
{item.name || '—'}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import {
} from '/@/renderer/store/settings.store';
import { Play } from '/@/renderer/types';
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';

const isFullWidthRow = (node: RowNode) => {
return node.id?.startsWith('disc-');
Expand Down Expand Up @@ -81,6 +82,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
const isFocused = useAppFocus();
const currentSong = useCurrentSong();
const { externalLinks } = useGeneralSettings();
const genreRoute = useGenreRoute();

const columnDefs = useMemo(
() => getColumnDefs(tableConfig.columns, false, 'albumDetail'),
Expand Down Expand Up @@ -389,7 +391,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
component={Link}
radius={0}
size="md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, {
to={generatePath(genreRoute, {
genreId: genre.id,
})}
variant="outline"
Expand Down
79 changes: 49 additions & 30 deletions src/renderer/features/albums/components/album-list-header.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,63 @@
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 { Flex, Group, Stack } from '@mantine/core';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { useListFilterRefresh } from '../../../hooks/use-list-filter-refresh';
import { LibraryItem } from '/@/renderer/api/types';
import { PageHeader, SearchInput } from '/@/renderer/components';
import { Button, PageHeader, SearchInput } from '/@/renderer/components';
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 { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import {
AlbumListFilter,
useCurrentServer,
useListStoreActions,
useListStoreByKey,
usePlayButtonBehavior,
} from '/@/renderer/store';
import { ListDisplayType } from '/@/renderer/types';
import { AlbumListFilter, useCurrentServer, usePlayButtonBehavior } from '/@/renderer/store';
import { titleCase } from '/@/renderer/utils';
import { RiMusicLine } from 'react-icons/ri';
import { generatePath } from 'react-router';
import { Link } from 'react-router-dom';
import { AppRoute } from '/@/renderer/router/routes';
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';

interface AlbumListHeaderProps {
genreId?: string;
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
title?: string;
}

export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumListHeaderProps) => {
export const AlbumListHeader = ({
genreId,
itemCount,
gridRef,
tableRef,
title,
}: AlbumListHeaderProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
const { setFilter, setTablePagination } = useListStoreActions();
const cq = useContainerQuery();
const { pageKey, handlePlay } = useListContext();
const { display, filter } = useListStoreByKey({ key: pageKey });
const playButtonBehavior = usePlayButtonBehavior();

const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
const genreRef = useRef<string>();
const { filter, handlePlay, refresh, search } = useDisplayRefresh({
gridRef,
itemType: LibraryItem.ALBUM,
server,
tableRef,
});

const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
const searchTerm = e.target.value === '' ? undefined : e.target.value;
const updatedFilters = setFilter({
data: { searchTerm },
itemType: LibraryItem.ALBUM,
key: pageKey,
}) as AlbumListFilter;
const updatedFilters = search(e) as AlbumListFilter;

if (display === ListDisplayType.TABLE || display === ListDisplayType.TABLE_PAGINATED) {
handleRefreshTable(tableRef, updatedFilters);
setTablePagination({ data: { currentPage: 0 }, key: pageKey });
} else {
handleRefreshGrid(gridRef, updatedFilters);
}
refresh(updatedFilters);
}, 500);

useEffect(() => {
if (genreRef.current && genreRef.current !== genreId) {
refresh(filter);
}

genreRef.current = genreId;
}, [filter, genreId, refresh, tableRef]);

return (
<Stack
ref={cq.ref}
Expand All @@ -81,6 +81,25 @@ export const AlbumListHeader = ({ itemCount, gridRef, tableRef, title }: AlbumLi
>
{itemCount}
</LibraryHeaderBar.Badge>
{genreId && (
<Button
compact
component={Link}
radius={0}
size="md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, {
genreId,
})}
tooltip={{
label: t('page.albumList.showTracks', {
postProcess: 'sentenceCase',
}),
}}
variant="filled"
>
<RiMusicLine />
</Button>
)}
</LibraryHeaderBar>
<Group>
<SearchInput
Expand Down
56 changes: 50 additions & 6 deletions src/renderer/features/albums/routes/album-list-route.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useCallback, useMemo, useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import isEmpty from 'lodash/isEmpty';
import { useTranslation } from 'react-i18next';
import { useParams, useSearchParams } from 'react-router-dom';
import { api } from '/@/renderer/api';
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 { ListContext } from '/@/renderer/context/list-context';
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
Expand All @@ -15,33 +16,68 @@ import { AnimatedPage } from '/@/renderer/features/shared';
import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer, useListFilterByKey } from '/@/renderer/store';
import { Play } from '/@/renderer/types';
import { useGenreList } from '/@/renderer/features/genres';
import { titleCase } from '/@/renderer/utils';

const AlbumListRoute = () => {
const { t } = useTranslation();
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
const tableRef = useRef<AgGridReactType | null>(null);
const server = useCurrentServer();
const [searchParams] = useSearchParams();
const { albumArtistId } = useParams();
const { albumArtistId, genreId } = useParams();
const pageKey = albumArtistId ? `albumArtistAlbum` : 'album';
const handlePlayQueueAdd = usePlayQueueAdd();

const customFilters = useMemo(() => {
const value = {
...(albumArtistId && { artistIds: [albumArtistId] }),
...(genreId && {
_custom: {
jellyfin: {
GenreIds: genreId,
},
navidrome: {
genre_id: genreId,
},
},
}),
};

if (isEmpty(value)) {
return undefined;
}

return value;
}, [albumArtistId]);
}, [albumArtistId, genreId]);

const albumListFilter = useListFilterByKey({
filter: customFilters,
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({
options: {
cacheTime: 1000 * 60,
Expand Down Expand Up @@ -98,19 +134,27 @@ const AlbumListRoute = () => {
return {
customFilters,
handlePlay,
id: albumArtistId ?? undefined,
id: albumArtistId ?? genreId,
pageKey,
};
}, [albumArtistId, customFilters, handlePlay, pageKey]);
}, [albumArtistId, customFilters, genreId, handlePlay, pageKey]);

const artist = searchParams.get('artistName');
const title = artist
? t('page.trackList.artistTracks', { artist })
: genreId
? t('page.albumList.genreAlbums', { genre: titleCase(genreTitle) })
: undefined;

return (
<AnimatedPage>
<ListContext.Provider value={providerValue}>
<AlbumListHeader
genreId={genreId}
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
title={searchParams.get('artistName') || undefined}
title={title}
/>
<AlbumListContent
gridRef={gridRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,11 @@ import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components
import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import {
GenreTarget,
useGeneralSettings,
usePlayButtonBehavior,
} from '/@/renderer/store/settings.store';
import { CardRow, Play, TableColumn } from '/@/renderer/types';
import { sanitize } from '/@/renderer/utils/sanitize';

Expand All @@ -64,7 +68,7 @@ interface AlbumArtistDetailContentProps {

export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => {
const { t } = useTranslation();
const { externalLinks } = useGeneralSettings();
const { externalLinks, genreTarget } = useGeneralSettings();
const { albumArtistId } = useParams() as { albumArtistId: string };
const cq = useContainerQuery();
const handlePlayQueueAdd = usePlayQueueAdd();
Expand Down Expand Up @@ -414,9 +418,14 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
component={Link}
radius="md"
size="md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, {
genreId: genre.id,
})}
to={generatePath(
genreTarget === GenreTarget.ALBUM
? AppRoute.LIBRARY_GENRES_ALBUMS
: AppRoute.LIBRARY_GENRES_SONGS,
{
genreId: genre.id,
},
)}
variant="outline"
>
{genre.name}
Expand Down
Loading

0 comments on commit 602fe05

Please sign in to comment.