temp commit for breaking changes

This commit is contained in:
jeffvli
2025-10-13 20:17:36 -07:00
parent 56907436a3
commit 6ff5affb58
30 changed files with 112 additions and 1423 deletions
+2 -3
View File
@@ -4,7 +4,6 @@ import { generatePath, Link } from 'react-router-dom';
import styles from './poster-card.module.css';
import { CardRows } from '/@/renderer/components/card/card-rows';
import { GridCardControls } from '/@/renderer/components/virtual-grid/grid-card/grid-card-controls';
import { Image } from '/@/shared/components/image/image';
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
import { Stack } from '/@/shared/components/stack/stack';
@@ -57,12 +56,12 @@ export const PosterCard = ({
>
<Link className={styles.imageContainer} to={path}>
<Image className={styles.image} src={data?.imageUrl} />
<GridCardControls
{/* <GridCardControls
handleFavorite={controls.handleFavorite}
isHovered={isHovered}
itemData={data}
itemType={controls.itemType}
/>
/> */}
</Link>
<div className={styles.detailContainer}>
<CardRows data={data} rows={controls.cardRows} />
@@ -30,7 +30,7 @@ import {
LibraryItem,
ServerListItem,
} from '/@/shared/types/domain-types';
import { ListDisplayType, TablePagination } from '/@/shared/types/types';
import { ListDisplayType, ListPagination } from '/@/shared/types/types';
export type AgGridFetchFn<TResponse, TFilter> = (
args: { filter: TFilter; limit: number; startIndex: number },
@@ -227,7 +227,7 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
);
const setParamsTablePagination = useCallback(
(args: { data: Partial<TablePagination>; key: ListKey }) => {
(args: { data: Partial<ListPagination>; key: ListKey }) => {
const { data } = args;
setSearchParams(
@@ -50,7 +50,7 @@ import {
import {
PlayerStatus,
TableColumn,
TablePagination as TablePaginationType,
ListPagination as TablePaginationType,
} from '/@/shared/types/types';
export * from './hooks/use-click-outside-deselect';
@@ -8,13 +8,17 @@ import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
import { Option } from '/@/shared/components/option/option';
import { Slider } from '/@/shared/components/slider/slider';
import { Switch } from '/@/shared/components/switch/switch';
import { TableColumn, TableType } from '/@/shared/types/types';
import { ItemListKey, TableColumn } from '/@/shared/types/types';
export const SONG_TABLE_COLUMNS = [
{
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
value: TableColumn.ROW_INDEX,
},
{
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
value: TableColumn.IMAGE,
},
{
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
value: TableColumn.TITLE,
@@ -43,6 +47,10 @@ export const SONG_TABLE_COLUMNS = [
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
value: TableColumn.GENRE,
},
{
label: i18n.t('table.config.label.genreBadge', { postProcess: 'titleCase' }),
value: TableColumn.GENRE_BADGE,
},
{
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
value: TableColumn.YEAR,
@@ -119,6 +127,10 @@ export const ALBUM_TABLE_COLUMNS = [
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
value: TableColumn.ROW_INDEX,
},
{
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
value: TableColumn.IMAGE,
},
{
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
value: TableColumn.TITLE,
@@ -147,6 +159,10 @@ export const ALBUM_TABLE_COLUMNS = [
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
value: TableColumn.GENRE,
},
{
label: i18n.t('table.config.label.genreBadge', { postProcess: 'titleCase' }),
value: TableColumn.GENRE_BADGE,
},
{
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
value: TableColumn.YEAR,
@@ -241,6 +257,10 @@ export const PLAYLIST_TABLE_COLUMNS = [
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
value: TableColumn.ROW_INDEX,
},
{
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
value: TableColumn.IMAGE,
},
{
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
value: TableColumn.TITLE,
@@ -284,23 +304,23 @@ export const GENRE_TABLE_COLUMNS = [
interface TableConfigDropdownProps {
// tableRef?: MutableRefObject<AgGridReactType<any> | null>;
type: TableType;
type: ItemListKey;
}
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const { t } = useTranslation();
const { setSettings } = useSettingsStoreActions();
const tableConfig = useSettingsStore((state) => state.tables);
const tableConfig = useSettingsStore((state) => state.lists);
const handleAddOrRemoveColumns = (values: string[]) => {
const existingColumns = tableConfig[type].columns;
if (values.length === 0) {
setSettings({
tables: {
...useSettingsStore.getState().tables,
lists: {
...useSettingsStore.getState().lists,
[type]: {
...useSettingsStore.getState().tables[type],
...useSettingsStore.getState().lists[type],
columns: [],
},
},
@@ -312,10 +332,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
if (values.length > existingColumns.length) {
const newColumn = { column: values[values.length - 1] };
setSettings({
tables: {
...useSettingsStore.getState().tables,
lists: {
...useSettingsStore.getState().lists,
[type]: {
...useSettingsStore.getState().tables[type],
...useSettingsStore.getState().lists[type],
columns: [...existingColumns, newColumn],
},
},
@@ -328,10 +348,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const newColumns = existingColumns.filter((column) => !removed.includes(column));
setSettings({
tables: {
...useSettingsStore.getState().tables,
lists: {
...useSettingsStore.getState().lists,
[type]: {
...useSettingsStore.getState().tables[type],
...useSettingsStore.getState().lists[type],
columns: newColumns,
},
},
@@ -341,10 +361,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const handleUpdateRowHeight = (value: number) => {
setSettings({
tables: {
...useSettingsStore.getState().tables,
lists: {
...useSettingsStore.getState().lists,
[type]: {
...useSettingsStore.getState().tables[type],
...useSettingsStore.getState().lists[type],
rowHeight: value,
},
},
@@ -353,10 +373,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const handleUpdateAutoFit = (e: ChangeEvent<HTMLInputElement>) => {
setSettings({
tables: {
...useSettingsStore.getState().tables,
lists: {
...useSettingsStore.getState().lists,
[type]: {
...useSettingsStore.getState().tables[type],
...useSettingsStore.getState().lists[type],
autoFit: e.currentTarget.checked,
},
},
@@ -365,10 +385,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
const handleUpdateFollow = (e: ChangeEvent<HTMLInputElement>) => {
setSettings({
tables: {
...useSettingsStore.getState().tables,
lists: {
...useSettingsStore.getState().lists,
[type]: {
...useSettingsStore.getState().tables[type],
...useSettingsStore.getState().lists[type],
followCurrentSong: e.currentTarget.checked,
},
},
@@ -14,7 +14,7 @@ import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Pagination } from '/@/shared/components/pagination/pagination';
import { Popover } from '/@/shared/components/popover/popover';
import { Text } from '/@/shared/components/text/text';
import { TablePagination as TablePaginationType } from '/@/shared/types/types';
import { ListPagination as TablePaginationType } from '/@/shared/types/types';
interface TablePaginationProps {
pageKey: ListKey;
@@ -1,81 +0,0 @@
import type { RowDoubleClickedEvent } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { MutableRefObject } from 'react';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { LibraryItem, QueueSong, SongListQuery } from '/@/shared/types/domain-types';
interface AlbumArtistSongListContentProps {
data: QueueSong[];
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const AlbumArtistDetailTopSongsListContent = ({
data,
tableRef,
}: AlbumArtistSongListContentProps) => {
const server = useCurrentServer();
const { id, pageKey } = useListContext();
const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior();
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data) return;
const rowData: QueueSong[] = [];
e.api.forEachNode((node) => {
if (!node.data) return;
rowData.push(node.data);
});
handlePlayQueueAdd?.({
byData: rowData,
initialSongId: e.data.id,
playType: playButtonBehavior,
});
};
const customFilters: Partial<SongListQuery> = {
...(id && { artistIds: [id] }),
};
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
const tableProps = useVirtualTable({
contextMenu: SONG_CONTEXT_MENU_ITEMS,
customFilters,
itemType: LibraryItem.SONG,
pageKey,
server,
tableRef,
});
return (
<>
<VirtualGridAutoSizerContainer>
<VirtualTable
key={`table-${tableProps.rowHeight}-${server?.id}`}
ref={tableRef}
shouldUpdateSong
{...tableProps}
getRowId={(data) => data.data.uniqueId}
onRowDoubleClicked={handleRowDoubleClick}
rowClassRules={rowClassRules}
rowData={data}
rowModelType="clientSide"
rowSelection="multiple"
/>
</VirtualGridAutoSizerContainer>
</>
);
};
@@ -1,177 +0,0 @@
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { MutableRefObject, useCallback, useMemo } from 'react';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { ListOnScrollProps } from 'react-window';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components/card/card-rows';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import {
VirtualInfiniteGrid,
VirtualInfiniteGridRef,
} from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import {
AlbumArtist,
AlbumArtistListQuery,
AlbumArtistListResponse,
AlbumArtistListSort,
LibraryItem,
} from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types';
interface AlbumArtistListGridViewProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
}
export const AlbumArtistListGridView = ({ gridRef, itemCount }: AlbumArtistListGridViewProps) => {
const queryClient = useQueryClient();
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { pageKey } = useListContext();
const { display, filter, grid } = useListStoreByKey<AlbumArtistListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const handleFavorite = useHandleFavorite({ gridRef });
const fetchInitialData = useCallback(() => {
const query: Omit<AlbumArtistListQuery, 'limit' | 'startIndex'> = {
...filter,
};
const queriesFromCache: [QueryKey, AlbumArtistListResponse | undefined][] =
queryClient.getQueriesData({
exact: false,
fetchStatus: 'idle',
queryKey: queryKeys.albumArtists.list(server?.id || '', query),
stale: false,
});
const itemData: AlbumArtist[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
}
}
return itemData;
}, [filter, queryClient, server?.id]);
const fetch = useCallback(
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
const query: AlbumArtistListQuery = {
...filter,
limit,
startIndex,
};
const queryKey = queryKeys.albumArtists.list(server?.id || '', query);
const albumArtistsRes = await queryClient.fetchQuery({
gcTime: 1000 * 60 * 1,
queryFn: async ({ signal }) =>
api.controller.getAlbumArtistList({
apiClientProps: {
serverId: server?.id || '',
signal,
},
query,
}),
queryKey,
});
return albumArtistsRes;
},
[filter, queryClient, server],
);
const handleGridScroll = useCallback(
(e: ListOnScrollProps) => {
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
},
[pageKey, setGrid],
);
const cardRows = useMemo(() => {
const rows: CardRow<AlbumArtist>[] = [ALBUMARTIST_CARD_ROWS.name];
switch (filter.sortBy) {
case AlbumArtistListSort.ALBUM_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.albumCount);
break;
case AlbumArtistListSort.DURATION:
rows.push(ALBUMARTIST_CARD_ROWS.duration);
break;
case AlbumArtistListSort.FAVORITED:
break;
case AlbumArtistListSort.NAME:
break;
case AlbumArtistListSort.PLAY_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.playCount);
break;
case AlbumArtistListSort.RANDOM:
break;
case AlbumArtistListSort.RATING:
rows.push(ALBUMARTIST_CARD_ROWS.rating);
break;
case AlbumArtistListSort.RECENTLY_ADDED:
break;
case AlbumArtistListSort.RELEASE_DATE:
break;
case AlbumArtistListSort.SONG_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.songCount);
break;
}
return rows;
}, [filter.sortBy]);
return (
<VirtualGridAutoSizerContainer>
<AutoSizer>
{({ height, width }: Size) => (
<VirtualInfiniteGrid
cardRows={cardRows}
display={display || ListDisplayType.CARD}
fetchFn={fetch}
fetchInitialData={fetchInitialData}
handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={grid?.scrollOffset || 0}
itemCount={itemCount || 0}
itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemSize || 200}
itemType={LibraryItem.ALBUM_ARTIST}
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
onScroll={handleGridScroll}
ref={gridRef}
route={{
route: AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'albumArtistId' }],
}}
width={width}
/>
)}
</AutoSizer>
</VirtualGridAutoSizerContainer>
);
};
@@ -1,45 +0,0 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { MutableRefObject } from 'react';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
import { ARTIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
interface AlbumArtistListTableViewProps {
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const AlbumArtistListTableView = ({
itemCount,
tableRef,
}: AlbumArtistListTableViewProps) => {
const server = useCurrentServer();
const { pageKey } = useListContext();
const tableProps = useVirtualTable({
contextMenu: ARTIST_CONTEXT_MENU_ITEMS,
itemCount,
itemType: LibraryItem.ALBUM_ARTIST,
pageKey,
server,
tableRef,
});
return (
<VirtualGridAutoSizerContainer>
<VirtualTable
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${tableProps.rowHeight}-${server?.id}`}
ref={tableRef}
{...tableProps}
/>
</VirtualGridAutoSizerContainer>
);
};
@@ -1,178 +0,0 @@
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { MutableRefObject, useCallback, useMemo } from 'react';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { ListOnScrollProps } from 'react-window';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { ALBUMARTIST_CARD_ROWS } from '/@/renderer/components/card/card-rows';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import {
VirtualInfiniteGrid,
VirtualInfiniteGridRef,
} from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { useListStoreByKey } from '/@/renderer/store/list.store';
import {
AlbumArtist,
ArtistListQuery,
ArtistListResponse,
ArtistListSort,
LibraryItem,
} from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types';
interface ArtistListGridViewProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
}
export const ArtistListGridView = ({ gridRef, itemCount }: ArtistListGridViewProps) => {
const queryClient = useQueryClient();
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { pageKey } = useListContext();
const { display, filter, grid } = useListStoreByKey<ArtistListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const handleFavorite = useHandleFavorite({ gridRef });
const fetchInitialData = useCallback(() => {
const query: Omit<ArtistListQuery, 'limit' | 'startIndex'> = {
...filter,
};
const queriesFromCache: [QueryKey, ArtistListResponse | undefined][] =
queryClient.getQueriesData({
exact: false,
fetchStatus: 'idle',
queryKey: queryKeys.artists.list(server?.id || '', query),
stale: false,
});
const itemData: AlbumArtist[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
}
}
return itemData;
}, [filter, queryClient, server?.id]);
const fetch = useCallback(
async ({ skip: startIndex, take: limit }: { skip: number; take: number }) => {
const query: ArtistListQuery = {
...filter,
limit,
startIndex,
};
const queryKey = queryKeys.artists.list(server?.id || '', query);
const artistsRes = await queryClient.fetchQuery({
gcTime: 1000 * 60 * 1,
queryFn: async ({ signal }) =>
api.controller.getArtistList({
apiClientProps: {
serverId: server?.id || '',
signal,
},
query,
}),
queryKey,
});
return artistsRes;
},
[filter, queryClient, server],
);
const handleGridScroll = useCallback(
(e: ListOnScrollProps) => {
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
},
[pageKey, setGrid],
);
const cardRows = useMemo(() => {
const rows: CardRow<AlbumArtist>[] = [ALBUMARTIST_CARD_ROWS.name];
switch (filter.sortBy) {
case ArtistListSort.ALBUM_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.albumCount);
break;
case ArtistListSort.DURATION:
rows.push(ALBUMARTIST_CARD_ROWS.duration);
break;
case ArtistListSort.FAVORITED:
break;
case ArtistListSort.NAME:
break;
case ArtistListSort.PLAY_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.playCount);
break;
case ArtistListSort.RANDOM:
break;
case ArtistListSort.RATING:
rows.push(ALBUMARTIST_CARD_ROWS.rating);
break;
case ArtistListSort.RECENTLY_ADDED:
break;
case ArtistListSort.RELEASE_DATE:
break;
case ArtistListSort.SONG_COUNT:
rows.push(ALBUMARTIST_CARD_ROWS.songCount);
break;
}
return rows;
}, [filter.sortBy]);
return (
<VirtualGridAutoSizerContainer>
<AutoSizer>
{({ height, width }: Size) => (
<VirtualInfiniteGrid
cardRows={cardRows}
display={display || ListDisplayType.CARD}
fetchFn={fetch}
fetchInitialData={fetchInitialData}
handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={grid?.scrollOffset || 0}
itemCount={itemCount || 0}
itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemSize || 200}
itemType={LibraryItem.ARTIST}
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
onScroll={handleGridScroll}
ref={gridRef}
route={{
route: AppRoute.LIBRARY_ARTISTS_DETAIL,
slugs: [{ idProperty: 'id', slugProperty: 'artistId' }],
}}
width={width}
/>
)}
</AutoSizer>
</VirtualGridAutoSizerContainer>
);
};
@@ -1,42 +0,0 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { MutableRefObject } from 'react';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
import { ARTIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
interface ArtistListTableViewProps {
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const ArtistListTableView = ({ itemCount, tableRef }: ArtistListTableViewProps) => {
const server = useCurrentServer();
const { pageKey } = useListContext();
const tableProps = useVirtualTable({
contextMenu: ARTIST_CONTEXT_MENU_ITEMS,
itemCount,
itemType: LibraryItem.ARTIST,
pageKey,
server,
tableRef,
});
return (
<VirtualGridAutoSizerContainer>
<VirtualTable
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${tableProps.rowHeight}-${server?.id}`}
ref={tableRef}
{...tableProps}
/>
</VirtualGridAutoSizerContainer>
);
};
@@ -6,7 +6,6 @@ import { useParams } from 'react-router';
import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistDetailTopSongsListContent } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-content';
import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { useCurrentServer } from '/@/renderer/store/auth.store';
@@ -54,10 +53,10 @@ const AlbumArtistDetailTopSongsListRoute = () => {
itemCount={itemCount}
title={detailQuery?.data?.name || 'Unknown'}
/>
<AlbumArtistDetailTopSongsListContent
{/* <AlbumArtistDetailTopSongsListContent
data={topSongsQuery?.data?.items || []}
tableRef={tableRef}
/>
/> */}
</ListContext.Provider>
</AnimatedPage>
);
@@ -3,10 +3,8 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistListContent } from '/@/renderer/features/artists/components/album-artist-list-content';
import { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { useCurrentServer } from '/@/renderer/store/auth.store';
@@ -14,7 +12,7 @@ import { useListFilterByKey } from '/@/renderer/store/list.store';
import { AlbumArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
const AlbumArtistListRoute = () => {
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
const gridRef = useRef<null>(null);
const tableRef = useRef<AgGridReactType | null>(null);
const pageKey = LibraryItem.ALBUM_ARTIST;
const server = useCurrentServer();
@@ -48,11 +46,11 @@ const AlbumArtistListRoute = () => {
itemCount={itemCount}
tableRef={tableRef}
/>
<AlbumArtistListContent
{/* <AlbumArtistListContent
gridRef={gridRef}
itemCount={itemCount}
tableRef={tableRef}
/>
/> */}
</ListContext.Provider>
</AnimatedPage>
);
@@ -3,7 +3,6 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { ArtistListContent } from '/@/renderer/features/artists/components/artist-list-content';
@@ -14,7 +13,7 @@ import { useListFilterByKey } from '/@/renderer/store/list.store';
import { ArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
const ArtistListRoute = () => {
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
const gridRef = useRef<null>(null);
const tableRef = useRef<AgGridReactType | null>(null);
const pageKey = LibraryItem.ARTIST;
const server = useCurrentServer();
@@ -45,7 +44,7 @@ const ArtistListRoute = () => {
<AnimatedPage>
<ListContext.Provider value={providerValue}>
<ArtistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
<ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
{/* <ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
</ListContext.Provider>
</AnimatedPage>
);
@@ -1,157 +0,0 @@
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { ListOnScrollProps } from 'react-window';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { ALBUM_CARD_ROWS } from '/@/renderer/components/card/card-rows';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualInfiniteGrid } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { useCurrentServer, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
import {
Album,
Genre,
GenreListQuery,
GenreListResponse,
LibraryItem,
} from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types';
export const GenreListGridView = ({ gridRef, itemCount }: any) => {
const queryClient = useQueryClient();
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { id, pageKey } = useListContext();
const { display, filter, grid } = useListStoreByKey<GenreListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const genrePath = useGenreRoute();
const [searchParams, setSearchParams] = useSearchParams();
const scrollOffset = searchParams.get('scrollOffset');
const initialScrollOffset = Number(id ? scrollOffset : grid?.scrollOffset) || 0;
const cardRows = useMemo(() => {
const rows: CardRow<Album>[] = [ALBUM_CARD_ROWS.name];
return rows;
}, []);
const handleGridScroll = useCallback(
(e: ListOnScrollProps) => {
if (id) {
setSearchParams(
(params) => {
params.set('scrollOffset', String(e.scrollOffset));
return params;
},
{ replace: true },
);
} else {
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
}
},
[id, pageKey, setGrid, setSearchParams],
);
const fetchInitialData = useCallback(() => {
const query: Omit<GenreListQuery, 'limit' | 'startIndex'> = {
...filter,
};
const queriesFromCache: [QueryKey, GenreListResponse | undefined][] =
queryClient.getQueriesData({
exact: false,
fetchStatus: 'idle',
queryKey: queryKeys.genres.list(server?.id || '', query),
stale: false,
});
const itemData: Genre[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
}
}
return itemData;
}, [filter, queryClient, server?.id]);
const fetch = useCallback(
async ({ skip, take }: { skip: number; take: number }) => {
if (!server) {
return [];
}
const query: GenreListQuery = {
...filter,
limit: take,
startIndex: skip,
};
const queryKey = queryKeys.albums.list(server?.id || '', query);
const albums = await queryClient.fetchQuery({
queryFn: async ({ signal }) => {
return api.controller.getGenreList({
apiClientProps: {
serverId: server?.id || '',
signal,
},
query,
});
},
queryKey,
});
return albums;
},
[filter, queryClient, server],
);
return (
<VirtualGridAutoSizerContainer>
<AutoSizer>
{({ height, width }: Size) => (
<VirtualInfiniteGrid
cardRows={cardRows}
display={display || ListDisplayType.CARD}
fetchFn={fetch}
fetchInitialData={fetchInitialData}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={initialScrollOffset}
itemCount={itemCount || 0}
itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemSize || 200}
itemType={LibraryItem.GENRE}
key={`album-list-${server?.id}-${display}`}
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
onScroll={handleGridScroll}
ref={gridRef}
route={{
route: genrePath,
slugs: [{ idProperty: 'id', slugProperty: 'genreId' }],
}}
width={width}
/>
)}
</AutoSizer>
</VirtualGridAutoSizerContainer>
);
};
@@ -1,59 +0,0 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { RowDoubleClickedEvent } from '@ag-grid-community/core';
import { MutableRefObject, useCallback } from 'react';
import { generatePath, useNavigate } from 'react-router';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { useListContext } from '/@/renderer/context/list-context';
import { GENRE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
interface GenreListTableViewProps {
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const GenreListTableView = ({ itemCount, tableRef }: GenreListTableViewProps) => {
const server = useCurrentServer();
const { customFilters, pageKey } = useListContext();
const navigate = useNavigate();
const genrePath = useGenreRoute();
const tableProps = useVirtualTable({
contextMenu: GENRE_CONTEXT_MENU_ITEMS,
customFilters,
itemCount,
itemType: LibraryItem.GENRE,
pageKey,
server,
tableRef,
});
const onRowDoubleClicked = useCallback(
(e: RowDoubleClickedEvent) => {
const { data } = e;
if (!data) return;
navigate(generatePath(genrePath, { genreId: data.id }));
},
[genrePath, navigate],
);
return (
<VirtualGridAutoSizerContainer>
<VirtualTable
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${tableProps.rowHeight}-${server?.id}`}
ref={tableRef}
{...tableProps}
onRowDoubleClicked={onRowDoubleClicked}
/>
</VirtualGridAutoSizerContainer>
);
};
@@ -3,10 +3,8 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
import { GenreListContent } from '/@/renderer/features/genres/components/genre-list-content';
import { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
import { useCurrentServer } from '/@/renderer/store';
@@ -14,7 +12,7 @@ import { useListStoreByKey } from '/@/renderer/store/list.store';
import { GenreListQuery } from '/@/shared/types/domain-types';
const GenreListRoute = () => {
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
const gridRef = useRef<null>(null);
const tableRef = useRef<AgGridReactType | null>(null);
const server = useCurrentServer();
const pageKey = 'genre';
@@ -46,7 +44,7 @@ const GenreListRoute = () => {
<AnimatedPage>
<ListContext.Provider value={providerValue}>
<GenreListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
<GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
{/* <GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
</ListContext.Provider>
</AnimatedPage>
);
@@ -12,7 +12,7 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
import { Group } from '/@/shared/components/group/group';
import { Popover } from '/@/shared/components/popover/popover';
import { Song } from '/@/shared/types/domain-types';
import { TableType } from '/@/shared/types/types';
import { ItemListKey } from '/@/shared/types/types';
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
@@ -20,7 +20,7 @@ interface PlayQueueListOptionsProps {
handleSearch: (value: string) => void;
searchTerm?: string;
tableRef: MutableRefObject<null | { grid: AgGridReactType<Song> }>;
type: TableType;
type: ItemListKey;
}
export const PlayQueueListControls = ({
@@ -8,7 +8,6 @@ import debounce from 'lodash/debounce';
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
@@ -21,18 +20,16 @@ import {
usePlayerStatus,
} from '/@/renderer/store';
import {
PersistedTableColumn,
useSettingsStore,
useSettingsStoreActions,
useTableSettings,
} from '/@/renderer/store/settings.store';
import { searchSongs } from '/@/renderer/utils/search-songs';
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
import { TableType } from '/@/shared/types/types';
import { ItemListKey } from '/@/shared/types/types';
type QueueProps = {
searchTerm?: string;
type: TableType;
type: ItemListKey;
};
export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<any>) => {
@@ -138,7 +135,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
const columnsOrder = columnApi?.getAllGridColumns();
if (!columnsOrder) return;
const columnsInSettings = useSettingsStore.getState().tables[type].columns;
const columnsInSettings = useSettingsStore.getState().lists[type].columns;
const updatedColumns: PersistedTableColumn[] = [];
for (const column of columnsOrder) {
@@ -149,7 +146,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
if (columnInSettings) {
updatedColumns.push({
...columnInSettings,
...(!useSettingsStore.getState().tables[type].autoFit && {
...(!useSettingsStore.getState().lists[type].autoFit && {
width: column.getActualWidth(),
}),
});
@@ -157,10 +154,10 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
}
setSettings({
tables: {
...useSettingsStore.getState().tables,
lists: {
...useSettingsStore.getState().lists,
[type]: {
...useSettingsStore.getState().tables[type],
...useSettingsStore.getState().lists[type],
columns: updatedColumns,
},
},
@@ -2,12 +2,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { useRef, useState } from 'react';
import { PlayQueueListControls } from './play-queue-list-controls';
import { VirtualGridContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import { useWindowSettings } from '/@/renderer/store/settings.store';
import { Box } from '/@/shared/components/box/box';
import { Song } from '/@/shared/types/domain-types';
import { Platform } from '/@/shared/types/types';
@@ -17,17 +12,6 @@ export const SidebarPlayQueue = () => {
const { windowBarStyle } = useWindowSettings();
const isWeb = windowBarStyle === Platform.WEB;
return (
<VirtualGridContainer>
<Box display={!isWeb ? 'flex' : undefined} h="65px">
<PlayQueueListControls
handleSearch={setSearch}
searchTerm={search}
tableRef={queueRef}
type="sideQueue"
/>
</Box>
<PlayQueue ref={queueRef} searchTerm={search} type="sideQueue" />
</VirtualGridContainer>
);
return null;
};
@@ -3,10 +3,6 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
import { useRef, useState } from 'react';
import { VirtualGridContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { NowPlayingHeader } from '/@/renderer/features/now-playing/components/now-playing-header';
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
const NowPlayingRoute = () => {
@@ -15,16 +11,11 @@ const NowPlayingRoute = () => {
return (
<AnimatedPage>
<VirtualGridContainer>
{/* <VirtualGridContainer>
<NowPlayingHeader />
<PlayQueueListControls
handleSearch={setSearch}
searchTerm={search}
tableRef={queueRef}
type="nowPlaying"
/>
<PlayQueue ref={queueRef} searchTerm={search} type="nowPlaying" />
</VirtualGridContainer>
<PlayQueueListControls tableRef={queueRef} type="nowPlaying" />
<PlayQueue ref={queueRef} type="nowPlaying" />
</VirtualGridContainer> */}
</AnimatedPage>
);
};
@@ -1,312 +0,0 @@
import type {
BodyScrollEvent,
ColDef,
GridReadyEvent,
PaginationChangedEvent,
RowDoubleClickedEvent,
RowDragEvent,
} from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import debounce from 'lodash/debounce';
import { AnimatePresence } from 'motion/react';
import { MutableRefObject, useCallback, useMemo } from 'react';
import { useParams } from 'react-router';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { getColumnDefs, TablePagination, VirtualTable } from '/@/renderer/components/virtual-table';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import {
PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { useAppFocus } from '/@/renderer/hooks';
import {
useCurrentServer,
usePlayerSong,
usePlayerStatus,
usePlaylistDetailStore,
usePlaylistDetailTablePagination,
useSetPlaylistDetailTable,
useSetPlaylistDetailTablePagination,
} from '/@/renderer/store';
import { PersistedTableColumn, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { toast } from '/@/shared/components/toast/toast';
import {
LibraryItem,
PlaylistSongListQueryClientSide,
QueueSong,
ServerType,
Song,
SongListResponse,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType } from '/@/shared/types/types';
interface PlaylistDetailContentProps {
songs?: Song[];
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetailContentProps) => {
const { playlistId } = useParams() as { playlistId: string };
const queryClient = useQueryClient();
const status = usePlayerStatus();
const isFocused = useAppFocus();
const currentSong = usePlayerSong();
const server = useCurrentServer();
const page = usePlaylistDetailStore();
const filters: PlaylistSongListQueryClientSide = useMemo(() => {
return {
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
};
}, [page?.table.id, playlistId]);
const detailQuery = useQuery(
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
);
const p = usePlaylistDetailTablePagination(playlistId);
const pagination = {
currentPage: p?.currentPage || 0,
itemsPerPage: p?.itemsPerPage || 100,
scrollOffset: p?.scrollOffset || 0,
totalItems: p?.totalItems || 1,
totalPages: p?.totalPages || 1,
};
const setPagination = useSetPlaylistDetailTablePagination();
const setTable = useSetPlaylistDetailTable();
const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior();
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
const columnDefs: ColDef[] = useMemo(
() => getColumnDefs(page.table.columns, false, 'generic'),
[page.table.columns],
);
const onGridReady = useCallback(
(params: GridReadyEvent) => {
params.api?.ensureIndexVisible(pagination.scrollOffset, 'top');
},
[pagination.scrollOffset],
);
const handleDragEnd = useCallback(
async (e: RowDragEvent<Song>) => {
if (!e.nodes.length) return;
const trackId = e.node.data?.playlistItemId;
if (trackId && e.node.rowIndex !== null && e.overIndex !== e.node.rowIndex) {
try {
await api.controller.movePlaylistItem({
apiClientProps: {
serverId: server?.id || '',
},
query: {
endingIndex: e.overIndex,
playlistId,
startingIndex: e.node.rowIndex + 1,
trackId,
},
});
queryClient.setQueryData<SongListResponse>(
queryKeys.playlists.songList(server?.id || '', playlistId),
(previous) => {
if (previous?.items) {
const from = e.node.rowIndex!;
const to = e.overIndex;
const item = previous.items[from];
const remaining = previous.items.toSpliced(from, 1);
remaining.splice(to, 0, item);
return {
error: previous.error,
items: remaining,
startIndex: previous.startIndex,
totalRecordCount: previous.totalRecordCount,
};
}
return previous;
},
);
// Nodes have to be redrawn, otherwise the row indexes will be wrong
// Maybe it's possible to only redraw necessary rows to not be as expensive?
tableRef.current?.api.redrawRows();
} catch (error) {
toast.error({
message: (error as Error).message,
title: `Failed to move song ${e.node.data?.name} to ${e.overIndex}`,
});
}
}
},
[playlistId, queryClient, server, tableRef],
);
const handleGridSizeChange = () => {
if (page.table.autoFit) {
tableRef?.current?.api?.sizeColumnsToFit();
}
};
const onPaginationChanged = useCallback(
(event: PaginationChangedEvent) => {
if (!isPaginationEnabled || !event.api) return;
try {
// Scroll to top of page on pagination change
const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage;
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
} catch (err) {
console.error(err);
}
setPagination(playlistId, {
itemsPerPage: event.api.paginationGetPageSize(),
totalItems: event.api.paginationGetRowCount(),
totalPages: event.api.paginationGetTotalPages() + 1,
});
},
[
isPaginationEnabled,
pagination.currentPage,
pagination.itemsPerPage,
playlistId,
setPagination,
],
);
const handleColumnChange = useCallback(() => {
const { columnApi } = tableRef?.current || {};
const columnsOrder = columnApi?.getAllGridColumns();
if (!columnsOrder) return;
const columnsInSettings = page.table.columns;
const updatedColumns: PersistedTableColumn[] = [];
for (const column of columnsOrder) {
const columnInSettings = columnsInSettings.find(
(c) => c.column === column.getColDef().colId,
);
if (columnInSettings) {
updatedColumns.push({
...columnInSettings,
...(!page.table.autoFit && {
width: column.getActualWidth(),
}),
});
}
}
setTable({ columns: updatedColumns });
}, [page.table.autoFit, page.table.columns, setTable, tableRef]);
const debouncedColumnChange = debounce(handleColumnChange, 200);
const handleScroll = (e: BodyScrollEvent) => {
const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0));
setPagination(playlistId, { scrollOffset });
};
const contextMenuItems = useMemo(() => {
if (detailQuery?.data?.rules) {
return SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
}
return PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
}, [detailQuery?.data?.rules]);
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, contextMenuItems, {
playlistId,
tableRef,
});
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data) return;
handlePlayQueueAdd?.({
byItemType: {
id: [playlistId],
type: LibraryItem.PLAYLIST,
},
initialSongId: e.data.id,
playType: playButtonBehavior,
});
};
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
const canDrag =
filters.sortBy === SongListSort.ID &&
!detailQuery?.data?.rules &&
server?.type !== ServerType.SUBSONIC;
return (
<>
<VirtualGridAutoSizerContainer>
<VirtualTable
alwaysShowHorizontalScroll
autoFitColumns={page.table.autoFit}
columnDefs={columnDefs}
context={{
currentSong,
isFocused,
itemType: LibraryItem.SONG,
onCellContextMenu: handleContextMenu,
status,
}}
getRowId={(data) => data.data.uniqueId}
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`}
onBodyScrollEnd={handleScroll}
onCellContextMenu={handleContextMenu}
onColumnMoved={handleColumnChange}
onColumnResized={debouncedColumnChange}
onGridReady={onGridReady}
onGridSizeChanged={handleGridSizeChange}
onPaginationChanged={onPaginationChanged}
onRowDoubleClicked={handleRowDoubleClick}
onRowDragEnd={handleDragEnd}
pagination={isPaginationEnabled}
paginationAutoPageSize={isPaginationEnabled}
paginationPageSize={pagination.itemsPerPage || 100}
ref={tableRef}
rowClassRules={rowClassRules}
rowData={songs}
rowDragEntireRow={canDrag}
rowHeight={page.table.rowHeight || 40}
rowModelType="clientSide"
shouldUpdateSong
/>
</VirtualGridAutoSizerContainer>
{isPaginationEnabled && (
<AnimatePresence initial={false} mode="wait" presenceAffectsLayout>
{page.display === ListDisplayType.TABLE_PAGINATED && (
<TablePagination
pageKey={playlistId}
pagination={pagination}
setIdPagination={setPagination}
tableRef={tableRef}
/>
)}
</AnimatePresence>
)}
</>
);
};
@@ -1,174 +0,0 @@
import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { MutableRefObject, useCallback, useMemo } from 'react';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { ListOnScrollProps } from 'react-window';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { PLAYLIST_CARD_ROWS } from '/@/renderer/components/card/card-rows';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import {
VirtualInfiniteGrid,
VirtualInfiniteGridRef,
} from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
import { useListStoreActions } from '/@/renderer/store/list.store';
import {
LibraryItem,
Playlist,
PlaylistListQuery,
PlaylistListResponse,
PlaylistListSort,
} from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types';
interface PlaylistListGridViewProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
}
export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridViewProps) => {
const { pageKey } = useListContext();
const queryClient = useQueryClient();
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { display, filter, grid } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const handleFavorite = useHandleFavorite({ gridRef });
const cardRows = useMemo(() => {
const rows: CardRow<Playlist>[] = [PLAYLIST_CARD_ROWS.nameFull];
switch (filter.sortBy) {
case PlaylistListSort.DURATION:
rows.push(PLAYLIST_CARD_ROWS.duration);
break;
case PlaylistListSort.NAME:
rows.push(PLAYLIST_CARD_ROWS.songCount);
break;
case PlaylistListSort.OWNER:
rows.push(PLAYLIST_CARD_ROWS.owner);
break;
case PlaylistListSort.PUBLIC:
rows.push(PLAYLIST_CARD_ROWS.public);
break;
case PlaylistListSort.SONG_COUNT:
rows.push(PLAYLIST_CARD_ROWS.songCount);
break;
case PlaylistListSort.UPDATED_AT:
break;
}
return rows;
}, [filter.sortBy]);
const handleGridScroll = useCallback(
(e: ListOnScrollProps) => {
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
},
[pageKey, setGrid],
);
const fetchInitialData = useCallback(() => {
const query: Omit<PlaylistListQuery, 'limit' | 'startIndex'> = {
...filter,
};
const queriesFromCache: [QueryKey, PlaylistListResponse | undefined][] =
queryClient.getQueriesData({
exact: false,
fetchStatus: 'idle',
queryKey: queryKeys.playlists.list(server?.id || '', query),
stale: false,
});
const itemData: Playlist[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
}
}
return itemData;
}, [filter, queryClient, server?.id]);
const fetch = useCallback(
async ({ skip, take }: { skip: number; take: number }) => {
if (!server) {
return [];
}
const query: PlaylistListQuery = {
limit: take,
...filter,
_custom: {},
startIndex: skip,
};
const queryKey = queryKeys.playlists.list(server?.id || '', query);
const playlists = await queryClient.fetchQuery({
queryFn: async ({ signal }) =>
controller.getPlaylistList({
apiClientProps: {
serverId: server?.id || '',
signal,
},
query,
}),
queryKey,
});
return playlists;
},
[filter, queryClient, server],
);
return (
<VirtualGridAutoSizerContainer>
<AutoSizer>
{({ height, width }: Size) => (
<VirtualInfiniteGrid
cardRows={cardRows}
display={display || ListDisplayType.CARD}
fetchFn={fetch}
fetchInitialData={fetchInitialData}
handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={grid?.scrollOffset || 0}
itemCount={itemCount || 0}
itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemSize || 200}
itemType={LibraryItem.PLAYLIST}
key={`playlist-list-${server?.id}`}
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
onScroll={handleGridScroll}
ref={gridRef}
route={{
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
}}
width={width}
/>
)}
</AutoSizer>
</VirtualGridAutoSizerContainer>
);
};
@@ -1,49 +0,0 @@
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { RowDoubleClickedEvent } from '@ag-grid-community/core';
import { MutableRefObject } from 'react';
import { generatePath, useNavigate } from 'react-router';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
interface PlaylistListTableViewProps {
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistListTableView = ({ itemCount, tableRef }: PlaylistListTableViewProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
const pageKey = 'playlist';
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
if (!e.data) return;
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }));
};
const tableProps = useVirtualTable({
contextMenu: PLAYLIST_CONTEXT_MENU_ITEMS,
itemCount,
itemType: LibraryItem.PLAYLIST,
pageKey,
server,
tableRef,
});
return (
<VirtualGridAutoSizerContainer>
<VirtualTable
key={`table-${tableProps.rowHeight}-${server?.id}`}
ref={tableRef}
{...tableProps}
onRowDoubleClicked={handleRowDoubleClick}
/>
</VirtualGridAutoSizerContainer>
);
};
@@ -9,7 +9,6 @@ import { generatePath, useNavigate, useParams } from 'react-router';
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistDetailSongListContent } from '/@/renderer/features/playlists/components/playlist-detail-song-list-content';
import { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
@@ -228,7 +227,7 @@ const PlaylistDetailSongListRoute = () => {
</Box>
</motion.div>
)}
<PlaylistDetailSongListContent songs={filterSortedSongs} tableRef={tableRef} />
{/* <PlaylistDetailSongListContent songs={filterSortedSongs} tableRef={tableRef} /> */}
</AnimatedPage>
);
};
@@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import { useParams } from 'react-router';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { ListContext } from '/@/renderer/context/list-context';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistListContent } from '/@/renderer/features/playlists/components/playlist-list-content';
@@ -14,7 +13,7 @@ import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
import { PlaylistListSort, PlaylistSongListQuery, SortOrder } from '/@/shared/types/domain-types';
const PlaylistListRoute = () => {
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
const gridRef = useRef<null>(null);
const tableRef = useRef<AgGridReactType | null>(null);
const server = useCurrentServer();
const { playlistId } = useParams();
@@ -53,7 +52,7 @@ const PlaylistListRoute = () => {
<AnimatedPage>
<ListContext.Provider value={providerValue}>
<PlaylistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
<PlaylistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
{/* <PlaylistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
</ListContext.Provider>
</AnimatedPage>
);
@@ -5,8 +5,6 @@ import { MutableRefObject } from 'react';
import { generatePath, useNavigate } from 'react-router';
import { useParams, useSearchParams } from 'react-router-dom';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import {
@@ -91,22 +89,5 @@ export const SearchContent = ({ tableRef }: SearchContentProps) => {
tableRef,
});
return (
<VirtualGridAutoSizerContainer>
<VirtualTable
{...tableProps}
context={{
itemType,
query: searchParams.get('query'),
}}
getRowId={(data) => data.data.id}
infiniteInitialRowCount={25}
key={`table-${itemType}-${tableProps.rowHeight}-${server?.id}`}
onRowDoubleClicked={handleRowDoubleClick}
ref={tableRef}
rowClassRules={rowClassRules}
shouldUpdateSong={itemType === LibraryItem.SONG}
/>
</VirtualGridAutoSizerContainer>
);
return null;
};
@@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
@@ -59,27 +58,25 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
<Spinner container size={25} />
) : (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<VirtualGridAutoSizerContainer>
<VirtualTable
autoFitColumns={tableConfig.autoFit}
columnDefs={columnDefs}
context={{
count,
itemType: LibraryItem.SONG,
onCellContextMenu,
song,
}}
deselectOnClickOutside={fullScreen}
getRowId={(data) => data.data.uniqueId}
onCellContextMenu={onCellContextMenu}
onCellDoubleClicked={handleRowDoubleClick}
ref={tableRef}
rowBuffer={50}
rowData={songQuery.data ?? []}
rowHeight={tableConfig.rowHeight || 40}
shouldUpdateSong
/>
</VirtualGridAutoSizerContainer>
<VirtualTable
autoFitColumns={tableConfig.autoFit}
columnDefs={columnDefs}
context={{
count,
itemType: LibraryItem.SONG,
onCellContextMenu,
song,
}}
deselectOnClickOutside={fullScreen}
getRowId={(data) => data.data.uniqueId}
onCellContextMenu={onCellContextMenu}
onCellDoubleClicked={handleRowDoubleClick}
ref={tableRef}
rowBuffer={50}
rowData={songQuery.data ?? []}
rowHeight={tableConfig.rowHeight || 40}
shouldUpdateSong
/>
</ErrorBoundary>
);
};
+2 -2
View File
@@ -5,7 +5,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
import { DataTableProps } from '/@/renderer/store/settings.store';
import { mergeOverridingColumns } from '/@/renderer/store/utils';
import { AlbumArtistListArgs, AlbumArtistListSort, SortOrder } from '/@/shared/types/domain-types';
import { ListDisplayType, TableColumn, TablePagination } from '/@/shared/types/types';
import { ListDisplayType, ListPagination, TableColumn } from '/@/shared/types/types';
export type AlbumArtistListFilter = Omit<AlbumArtistListArgs['query'], 'limit' | 'startIndex'>;
@@ -33,7 +33,7 @@ type ListProps<T> = {
};
type TableProps = DataTableProps & {
pagination: TablePagination;
pagination: ListPagination;
scrollOffset: number;
};
+12 -10
View File
@@ -3,7 +3,9 @@ import { immer } from 'zustand/middleware/immer';
import { shallow } from 'zustand/shallow';
import { createWithEqualityFn } from 'zustand/traditional';
import { DataTableProps, PersistedTableColumn } from '/@/renderer/store/settings.store';
import { ItemTableListColumnConfig } from '/@/renderer/components/item-list/types';
import { ListDeterministicArgs } from '/@/renderer/store';
import { DataTableProps } from '/@/renderer/store/settings.store';
import { mergeOverridingColumns } from '/@/renderer/store/utils';
import {
AlbumArtistListArgs,
@@ -20,7 +22,7 @@ import {
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType, TableColumn, TablePagination } from '/@/shared/types/types';
import { ListDisplayType, ListPagination, TableColumn } from '/@/shared/types/types';
export const generatePageKey = (page: string, id?: string) => {
return id ? `${page}_${id}` : page;
@@ -35,13 +37,14 @@ export type ListGridProps = {
itemGap?: number;
itemSize?: number;
itemsPerRow?: number;
scrollOffset?: number;
};
export type ListItemProps<TFilter = any> = {
display: ListDisplayType;
filter: TFilter;
grid?: ListGridProps;
grid: ListGridProps;
itemsPerPage: number;
pagination: ListPagination;
table: ListTableProps;
};
@@ -67,9 +70,11 @@ export interface ListSlice extends ListState {
setGrid: (args: ListDeterministicArgs & { data: Partial<ListGridProps> }) => void;
setStore: (data: Partial<ListSlice>) => void;
setTable: (args: ListDeterministicArgs & { data: Partial<ListTableProps> }) => void;
setTableColumns: (args: ListDeterministicArgs & { data: PersistedTableColumn[] }) => void;
setTableColumns: (
args: ListDeterministicArgs & { data: ItemTableListColumnConfig[] },
) => void;
setTablePagination: (
args: ListDeterministicArgs & { data: Partial<TablePagination> },
args: ListDeterministicArgs & { data: Partial<ListPagination> },
) => void;
};
}
@@ -90,10 +95,7 @@ export interface ListState {
};
}
export type ListTableProps = DataTableProps & {
pagination: TablePagination;
scrollOffset: number;
};
export type ListTableProps = DataTableProps;
export type PlaylistListFilter = Omit<PlaylistListArgs['query'], 'limit' | 'startIndex'>;
+4 -4
View File
@@ -6,7 +6,7 @@ import { PlaylistListFilter, SongListFilter } from '/@/renderer/store/list.store
import { DataTableProps } from '/@/renderer/store/settings.store';
import { mergeOverridingColumns } from '/@/renderer/store/utils';
import { PlaylistListSort, SortOrder } from '/@/shared/types/domain-types';
import { ListDisplayType, TableColumn, TablePagination } from '/@/shared/types/types';
import { ListDisplayType, TableColumn, ListPagination } from '/@/shared/types/types';
export interface PlaylistSlice extends PlaylistState {
actions: {
@@ -17,11 +17,11 @@ export interface PlaylistSlice extends PlaylistState {
setGrid: (args: { data: Partial<ListGridProps> }) => void;
setStore: (data: Partial<PlaylistSlice>) => void;
setTable: (data: Partial<TableProps>) => void;
setTablePagination: (args: { data: Partial<TablePagination> }) => void;
setTablePagination: (args: { data: Partial<ListPagination> }) => void;
};
}
type DetailPaginationProps = TablePagination & {
type DetailPaginationProps = ListPagination & {
scrollOffset: number;
};
@@ -54,7 +54,7 @@ interface PlaylistState {
}
type TableProps = DataTableProps & {
pagination: TablePagination;
pagination: ListPagination;
scrollOffset: number;
};