mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
temp commit for breaking changes
This commit is contained in:
@@ -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;
|
||||
|
||||
-81
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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'>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user