mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +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 styles from './poster-card.module.css';
|
||||||
|
|
||||||
import { CardRows } from '/@/renderer/components/card/card-rows';
|
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 { Image } from '/@/shared/components/image/image';
|
||||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
@@ -57,12 +56,12 @@ export const PosterCard = ({
|
|||||||
>
|
>
|
||||||
<Link className={styles.imageContainer} to={path}>
|
<Link className={styles.imageContainer} to={path}>
|
||||||
<Image className={styles.image} src={data?.imageUrl} />
|
<Image className={styles.image} src={data?.imageUrl} />
|
||||||
<GridCardControls
|
{/* <GridCardControls
|
||||||
handleFavorite={controls.handleFavorite}
|
handleFavorite={controls.handleFavorite}
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
itemData={data}
|
itemData={data}
|
||||||
itemType={controls.itemType}
|
itemType={controls.itemType}
|
||||||
/>
|
/> */}
|
||||||
</Link>
|
</Link>
|
||||||
<div className={styles.detailContainer}>
|
<div className={styles.detailContainer}>
|
||||||
<CardRows data={data} rows={controls.cardRows} />
|
<CardRows data={data} rows={controls.cardRows} />
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
LibraryItem,
|
LibraryItem,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
} from '/@/shared/types/domain-types';
|
} from '/@/shared/types/domain-types';
|
||||||
import { ListDisplayType, TablePagination } from '/@/shared/types/types';
|
import { ListDisplayType, ListPagination } from '/@/shared/types/types';
|
||||||
|
|
||||||
export type AgGridFetchFn<TResponse, TFilter> = (
|
export type AgGridFetchFn<TResponse, TFilter> = (
|
||||||
args: { filter: TFilter; limit: number; startIndex: number },
|
args: { filter: TFilter; limit: number; startIndex: number },
|
||||||
@@ -227,7 +227,7 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const setParamsTablePagination = useCallback(
|
const setParamsTablePagination = useCallback(
|
||||||
(args: { data: Partial<TablePagination>; key: ListKey }) => {
|
(args: { data: Partial<ListPagination>; key: ListKey }) => {
|
||||||
const { data } = args;
|
const { data } = args;
|
||||||
|
|
||||||
setSearchParams(
|
setSearchParams(
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
PlayerStatus,
|
PlayerStatus,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
TablePagination as TablePaginationType,
|
ListPagination as TablePaginationType,
|
||||||
} from '/@/shared/types/types';
|
} from '/@/shared/types/types';
|
||||||
|
|
||||||
export * from './hooks/use-click-outside-deselect';
|
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 { Option } from '/@/shared/components/option/option';
|
||||||
import { Slider } from '/@/shared/components/slider/slider';
|
import { Slider } from '/@/shared/components/slider/slider';
|
||||||
import { Switch } from '/@/shared/components/switch/switch';
|
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 = [
|
export const SONG_TABLE_COLUMNS = [
|
||||||
{
|
{
|
||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.ROW_INDEX,
|
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' }),
|
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.TITLE,
|
value: TableColumn.TITLE,
|
||||||
@@ -43,6 +47,10 @@ export const SONG_TABLE_COLUMNS = [
|
|||||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.GENRE,
|
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' }),
|
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.YEAR,
|
value: TableColumn.YEAR,
|
||||||
@@ -119,6 +127,10 @@ export const ALBUM_TABLE_COLUMNS = [
|
|||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.ROW_INDEX,
|
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' }),
|
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.TITLE,
|
value: TableColumn.TITLE,
|
||||||
@@ -147,6 +159,10 @@ export const ALBUM_TABLE_COLUMNS = [
|
|||||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.GENRE,
|
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' }),
|
label: i18n.t('table.config.label.year', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.YEAR,
|
value: TableColumn.YEAR,
|
||||||
@@ -241,6 +257,10 @@ export const PLAYLIST_TABLE_COLUMNS = [
|
|||||||
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.rowIndex', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.ROW_INDEX,
|
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' }),
|
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||||
value: TableColumn.TITLE,
|
value: TableColumn.TITLE,
|
||||||
@@ -284,23 +304,23 @@ export const GENRE_TABLE_COLUMNS = [
|
|||||||
|
|
||||||
interface TableConfigDropdownProps {
|
interface TableConfigDropdownProps {
|
||||||
// tableRef?: MutableRefObject<AgGridReactType<any> | null>;
|
// tableRef?: MutableRefObject<AgGridReactType<any> | null>;
|
||||||
type: TableType;
|
type: ItemListKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { setSettings } = useSettingsStoreActions();
|
const { setSettings } = useSettingsStoreActions();
|
||||||
const tableConfig = useSettingsStore((state) => state.tables);
|
const tableConfig = useSettingsStore((state) => state.lists);
|
||||||
|
|
||||||
const handleAddOrRemoveColumns = (values: string[]) => {
|
const handleAddOrRemoveColumns = (values: string[]) => {
|
||||||
const existingColumns = tableConfig[type].columns;
|
const existingColumns = tableConfig[type].columns;
|
||||||
|
|
||||||
if (values.length === 0) {
|
if (values.length === 0) {
|
||||||
setSettings({
|
setSettings({
|
||||||
tables: {
|
lists: {
|
||||||
...useSettingsStore.getState().tables,
|
...useSettingsStore.getState().lists,
|
||||||
[type]: {
|
[type]: {
|
||||||
...useSettingsStore.getState().tables[type],
|
...useSettingsStore.getState().lists[type],
|
||||||
columns: [],
|
columns: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -312,10 +332,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||||||
if (values.length > existingColumns.length) {
|
if (values.length > existingColumns.length) {
|
||||||
const newColumn = { column: values[values.length - 1] };
|
const newColumn = { column: values[values.length - 1] };
|
||||||
setSettings({
|
setSettings({
|
||||||
tables: {
|
lists: {
|
||||||
...useSettingsStore.getState().tables,
|
...useSettingsStore.getState().lists,
|
||||||
[type]: {
|
[type]: {
|
||||||
...useSettingsStore.getState().tables[type],
|
...useSettingsStore.getState().lists[type],
|
||||||
columns: [...existingColumns, newColumn],
|
columns: [...existingColumns, newColumn],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -328,10 +348,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||||
|
|
||||||
setSettings({
|
setSettings({
|
||||||
tables: {
|
lists: {
|
||||||
...useSettingsStore.getState().tables,
|
...useSettingsStore.getState().lists,
|
||||||
[type]: {
|
[type]: {
|
||||||
...useSettingsStore.getState().tables[type],
|
...useSettingsStore.getState().lists[type],
|
||||||
columns: newColumns,
|
columns: newColumns,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -341,10 +361,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||||||
|
|
||||||
const handleUpdateRowHeight = (value: number) => {
|
const handleUpdateRowHeight = (value: number) => {
|
||||||
setSettings({
|
setSettings({
|
||||||
tables: {
|
lists: {
|
||||||
...useSettingsStore.getState().tables,
|
...useSettingsStore.getState().lists,
|
||||||
[type]: {
|
[type]: {
|
||||||
...useSettingsStore.getState().tables[type],
|
...useSettingsStore.getState().lists[type],
|
||||||
rowHeight: value,
|
rowHeight: value,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -353,10 +373,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||||||
|
|
||||||
const handleUpdateAutoFit = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleUpdateAutoFit = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSettings({
|
setSettings({
|
||||||
tables: {
|
lists: {
|
||||||
...useSettingsStore.getState().tables,
|
...useSettingsStore.getState().lists,
|
||||||
[type]: {
|
[type]: {
|
||||||
...useSettingsStore.getState().tables[type],
|
...useSettingsStore.getState().lists[type],
|
||||||
autoFit: e.currentTarget.checked,
|
autoFit: e.currentTarget.checked,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -365,10 +385,10 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||||||
|
|
||||||
const handleUpdateFollow = (e: ChangeEvent<HTMLInputElement>) => {
|
const handleUpdateFollow = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setSettings({
|
setSettings({
|
||||||
tables: {
|
lists: {
|
||||||
...useSettingsStore.getState().tables,
|
...useSettingsStore.getState().lists,
|
||||||
[type]: {
|
[type]: {
|
||||||
...useSettingsStore.getState().tables[type],
|
...useSettingsStore.getState().lists[type],
|
||||||
followCurrentSong: e.currentTarget.checked,
|
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 { Pagination } from '/@/shared/components/pagination/pagination';
|
||||||
import { Popover } from '/@/shared/components/popover/popover';
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
import { Text } from '/@/shared/components/text/text';
|
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 {
|
interface TablePaginationProps {
|
||||||
pageKey: ListKey;
|
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 { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
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 { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
||||||
@@ -54,10 +53,10 @@ const AlbumArtistDetailTopSongsListRoute = () => {
|
|||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
title={detailQuery?.data?.name || 'Unknown'}
|
title={detailQuery?.data?.name || 'Unknown'}
|
||||||
/>
|
/>
|
||||||
<AlbumArtistDetailTopSongsListContent
|
{/* <AlbumArtistDetailTopSongsListContent
|
||||||
data={topSongsQuery?.data?.items || []}
|
data={topSongsQuery?.data?.items || []}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/> */}
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
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 { AlbumArtistListHeader } from '/@/renderer/features/artists/components/album-artist-list-header';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { useCurrentServer } from '/@/renderer/store/auth.store';
|
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';
|
import { AlbumArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const AlbumArtistListRoute = () => {
|
const AlbumArtistListRoute = () => {
|
||||||
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
|
const gridRef = useRef<null>(null);
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const pageKey = LibraryItem.ALBUM_ARTIST;
|
const pageKey = LibraryItem.ALBUM_ARTIST;
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
@@ -48,11 +46,11 @@ const AlbumArtistListRoute = () => {
|
|||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/>
|
||||||
<AlbumArtistListContent
|
{/* <AlbumArtistListContent
|
||||||
gridRef={gridRef}
|
gridRef={gridRef}
|
||||||
itemCount={itemCount}
|
itemCount={itemCount}
|
||||||
tableRef={tableRef}
|
tableRef={tableRef}
|
||||||
/>
|
/> */}
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
|
||||||
import { ArtistListContent } from '/@/renderer/features/artists/components/artist-list-content';
|
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';
|
import { ArtistListQuery, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const ArtistListRoute = () => {
|
const ArtistListRoute = () => {
|
||||||
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
|
const gridRef = useRef<null>(null);
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const pageKey = LibraryItem.ARTIST;
|
const pageKey = LibraryItem.ARTIST;
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
@@ -45,7 +44,7 @@ const ArtistListRoute = () => {
|
|||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<ArtistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
<ArtistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||||
<ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
{/* <ArtistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</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 { useQuery } from '@tanstack/react-query';
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
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 { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
@@ -14,7 +12,7 @@ import { useListStoreByKey } from '/@/renderer/store/list.store';
|
|||||||
import { GenreListQuery } from '/@/shared/types/domain-types';
|
import { GenreListQuery } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const GenreListRoute = () => {
|
const GenreListRoute = () => {
|
||||||
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
|
const gridRef = useRef<null>(null);
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const pageKey = 'genre';
|
const pageKey = 'genre';
|
||||||
@@ -46,7 +44,7 @@ const GenreListRoute = () => {
|
|||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<GenreListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
<GenreListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||||
<GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
{/* <GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Popover } from '/@/shared/components/popover/popover';
|
import { Popover } from '/@/shared/components/popover/popover';
|
||||||
import { Song } from '/@/shared/types/domain-types';
|
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;
|
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ interface PlayQueueListOptionsProps {
|
|||||||
handleSearch: (value: string) => void;
|
handleSearch: (value: string) => void;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
tableRef: MutableRefObject<null | { grid: AgGridReactType<Song> }>;
|
tableRef: MutableRefObject<null | { grid: AgGridReactType<Song> }>;
|
||||||
type: TableType;
|
type: ItemListKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PlayQueueListControls = ({
|
export const PlayQueueListControls = ({
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import debounce from 'lodash/debounce';
|
|||||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
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 { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||||
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
|
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
|
||||||
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||||
@@ -21,18 +20,16 @@ import {
|
|||||||
usePlayerStatus,
|
usePlayerStatus,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import {
|
import {
|
||||||
PersistedTableColumn,
|
|
||||||
useSettingsStore,
|
useSettingsStore,
|
||||||
useSettingsStoreActions,
|
useSettingsStoreActions,
|
||||||
useTableSettings,
|
useTableSettings,
|
||||||
} from '/@/renderer/store/settings.store';
|
} from '/@/renderer/store/settings.store';
|
||||||
import { searchSongs } from '/@/renderer/utils/search-songs';
|
import { searchSongs } from '/@/renderer/utils/search-songs';
|
||||||
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
|
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
|
||||||
import { TableType } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
type QueueProps = {
|
type QueueProps = {
|
||||||
searchTerm?: string;
|
type: ItemListKey;
|
||||||
type: TableType;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<any>) => {
|
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();
|
const columnsOrder = columnApi?.getAllGridColumns();
|
||||||
if (!columnsOrder) return;
|
if (!columnsOrder) return;
|
||||||
|
|
||||||
const columnsInSettings = useSettingsStore.getState().tables[type].columns;
|
const columnsInSettings = useSettingsStore.getState().lists[type].columns;
|
||||||
|
|
||||||
const updatedColumns: PersistedTableColumn[] = [];
|
const updatedColumns: PersistedTableColumn[] = [];
|
||||||
for (const column of columnsOrder) {
|
for (const column of columnsOrder) {
|
||||||
@@ -149,7 +146,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
|||||||
if (columnInSettings) {
|
if (columnInSettings) {
|
||||||
updatedColumns.push({
|
updatedColumns.push({
|
||||||
...columnInSettings,
|
...columnInSettings,
|
||||||
...(!useSettingsStore.getState().tables[type].autoFit && {
|
...(!useSettingsStore.getState().lists[type].autoFit && {
|
||||||
width: column.getActualWidth(),
|
width: column.getActualWidth(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -157,10 +154,10 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSettings({
|
setSettings({
|
||||||
tables: {
|
lists: {
|
||||||
...useSettingsStore.getState().tables,
|
...useSettingsStore.getState().lists,
|
||||||
[type]: {
|
[type]: {
|
||||||
...useSettingsStore.getState().tables[type],
|
...useSettingsStore.getState().lists[type],
|
||||||
columns: updatedColumns,
|
columns: updatedColumns,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
|||||||
|
|
||||||
import { useRef, useState } from 'react';
|
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 { useWindowSettings } from '/@/renderer/store/settings.store';
|
||||||
import { Box } from '/@/shared/components/box/box';
|
|
||||||
import { Song } from '/@/shared/types/domain-types';
|
import { Song } from '/@/shared/types/domain-types';
|
||||||
import { Platform } from '/@/shared/types/types';
|
import { Platform } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -17,17 +12,6 @@ export const SidebarPlayQueue = () => {
|
|||||||
const { windowBarStyle } = useWindowSettings();
|
const { windowBarStyle } = useWindowSettings();
|
||||||
|
|
||||||
const isWeb = windowBarStyle === Platform.WEB;
|
const isWeb = windowBarStyle === Platform.WEB;
|
||||||
return (
|
|
||||||
<VirtualGridContainer>
|
return null;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,10 +3,6 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
|||||||
|
|
||||||
import { useRef, useState } from 'react';
|
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';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
|
|
||||||
const NowPlayingRoute = () => {
|
const NowPlayingRoute = () => {
|
||||||
@@ -15,16 +11,11 @@ const NowPlayingRoute = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<VirtualGridContainer>
|
{/* <VirtualGridContainer>
|
||||||
<NowPlayingHeader />
|
<NowPlayingHeader />
|
||||||
<PlayQueueListControls
|
<PlayQueueListControls tableRef={queueRef} type="nowPlaying" />
|
||||||
handleSearch={setSearch}
|
<PlayQueue ref={queueRef} type="nowPlaying" />
|
||||||
searchTerm={search}
|
</VirtualGridContainer> */}
|
||||||
tableRef={queueRef}
|
|
||||||
type="nowPlaying"
|
|
||||||
/>
|
|
||||||
<PlayQueue ref={queueRef} searchTerm={search} type="nowPlaying" />
|
|
||||||
</VirtualGridContainer>
|
|
||||||
</AnimatedPage>
|
</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 { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
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 { PlaylistDetailSongListHeader } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header';
|
||||||
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
|
import { PlaylistQueryBuilder } from '/@/renderer/features/playlists/components/playlist-query-builder';
|
||||||
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
import { SaveAsPlaylistForm } from '/@/renderer/features/playlists/components/save-as-playlist-form';
|
||||||
@@ -228,7 +227,7 @@ const PlaylistDetailSongListRoute = () => {
|
|||||||
</Box>
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
<PlaylistDetailSongListContent songs={filterSortedSongs} tableRef={tableRef} />
|
{/* <PlaylistDetailSongListContent songs={filterSortedSongs} tableRef={tableRef} /> */}
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||||
import { PlaylistListContent } from '/@/renderer/features/playlists/components/playlist-list-content';
|
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';
|
import { PlaylistListSort, PlaylistSongListQuery, SortOrder } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const PlaylistListRoute = () => {
|
const PlaylistListRoute = () => {
|
||||||
const gridRef = useRef<null | VirtualInfiniteGridRef>(null);
|
const gridRef = useRef<null>(null);
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const { playlistId } = useParams();
|
const { playlistId } = useParams();
|
||||||
@@ -53,7 +52,7 @@ const PlaylistListRoute = () => {
|
|||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<PlaylistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
<PlaylistListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
||||||
<PlaylistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
{/* <PlaylistListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { MutableRefObject } from 'react';
|
|||||||
import { generatePath, useNavigate } from 'react-router';
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
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 { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
||||||
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
|
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
|
||||||
import {
|
import {
|
||||||
@@ -91,22 +89,5 @@ export const SearchContent = ({ tableRef }: SearchContentProps) => {
|
|||||||
tableRef,
|
tableRef,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return null;
|
||||||
<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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
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 { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||||
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
|
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
|
||||||
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
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} />
|
<Spinner container size={25} />
|
||||||
) : (
|
) : (
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||||
<VirtualGridAutoSizerContainer>
|
<VirtualTable
|
||||||
<VirtualTable
|
autoFitColumns={tableConfig.autoFit}
|
||||||
autoFitColumns={tableConfig.autoFit}
|
columnDefs={columnDefs}
|
||||||
columnDefs={columnDefs}
|
context={{
|
||||||
context={{
|
count,
|
||||||
count,
|
itemType: LibraryItem.SONG,
|
||||||
itemType: LibraryItem.SONG,
|
onCellContextMenu,
|
||||||
onCellContextMenu,
|
song,
|
||||||
song,
|
}}
|
||||||
}}
|
deselectOnClickOutside={fullScreen}
|
||||||
deselectOnClickOutside={fullScreen}
|
getRowId={(data) => data.data.uniqueId}
|
||||||
getRowId={(data) => data.data.uniqueId}
|
onCellContextMenu={onCellContextMenu}
|
||||||
onCellContextMenu={onCellContextMenu}
|
onCellDoubleClicked={handleRowDoubleClick}
|
||||||
onCellDoubleClicked={handleRowDoubleClick}
|
ref={tableRef}
|
||||||
ref={tableRef}
|
rowBuffer={50}
|
||||||
rowBuffer={50}
|
rowData={songQuery.data ?? []}
|
||||||
rowData={songQuery.data ?? []}
|
rowHeight={tableConfig.rowHeight || 40}
|
||||||
rowHeight={tableConfig.rowHeight || 40}
|
shouldUpdateSong
|
||||||
shouldUpdateSong
|
/>
|
||||||
/>
|
|
||||||
</VirtualGridAutoSizerContainer>
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
|||||||
import { DataTableProps } from '/@/renderer/store/settings.store';
|
import { DataTableProps } from '/@/renderer/store/settings.store';
|
||||||
import { mergeOverridingColumns } from '/@/renderer/store/utils';
|
import { mergeOverridingColumns } from '/@/renderer/store/utils';
|
||||||
import { AlbumArtistListArgs, AlbumArtistListSort, SortOrder } from '/@/shared/types/domain-types';
|
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'>;
|
export type AlbumArtistListFilter = Omit<AlbumArtistListArgs['query'], 'limit' | 'startIndex'>;
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ type ListProps<T> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type TableProps = DataTableProps & {
|
type TableProps = DataTableProps & {
|
||||||
pagination: TablePagination;
|
pagination: ListPagination;
|
||||||
scrollOffset: number;
|
scrollOffset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { immer } from 'zustand/middleware/immer';
|
|||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { createWithEqualityFn } from 'zustand/traditional';
|
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 { mergeOverridingColumns } from '/@/renderer/store/utils';
|
||||||
import {
|
import {
|
||||||
AlbumArtistListArgs,
|
AlbumArtistListArgs,
|
||||||
@@ -20,7 +22,7 @@ import {
|
|||||||
SongListSort,
|
SongListSort,
|
||||||
SortOrder,
|
SortOrder,
|
||||||
} from '/@/shared/types/domain-types';
|
} 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) => {
|
export const generatePageKey = (page: string, id?: string) => {
|
||||||
return id ? `${page}_${id}` : page;
|
return id ? `${page}_${id}` : page;
|
||||||
@@ -35,13 +37,14 @@ export type ListGridProps = {
|
|||||||
itemGap?: number;
|
itemGap?: number;
|
||||||
itemSize?: number;
|
itemSize?: number;
|
||||||
itemsPerRow?: number;
|
itemsPerRow?: number;
|
||||||
scrollOffset?: number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ListItemProps<TFilter = any> = {
|
export type ListItemProps<TFilter = any> = {
|
||||||
display: ListDisplayType;
|
display: ListDisplayType;
|
||||||
filter: TFilter;
|
filter: TFilter;
|
||||||
grid?: ListGridProps;
|
grid: ListGridProps;
|
||||||
|
itemsPerPage: number;
|
||||||
|
pagination: ListPagination;
|
||||||
table: ListTableProps;
|
table: ListTableProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,9 +70,11 @@ export interface ListSlice extends ListState {
|
|||||||
setGrid: (args: ListDeterministicArgs & { data: Partial<ListGridProps> }) => void;
|
setGrid: (args: ListDeterministicArgs & { data: Partial<ListGridProps> }) => void;
|
||||||
setStore: (data: Partial<ListSlice>) => void;
|
setStore: (data: Partial<ListSlice>) => void;
|
||||||
setTable: (args: ListDeterministicArgs & { data: Partial<ListTableProps> }) => void;
|
setTable: (args: ListDeterministicArgs & { data: Partial<ListTableProps> }) => void;
|
||||||
setTableColumns: (args: ListDeterministicArgs & { data: PersistedTableColumn[] }) => void;
|
setTableColumns: (
|
||||||
|
args: ListDeterministicArgs & { data: ItemTableListColumnConfig[] },
|
||||||
|
) => void;
|
||||||
setTablePagination: (
|
setTablePagination: (
|
||||||
args: ListDeterministicArgs & { data: Partial<TablePagination> },
|
args: ListDeterministicArgs & { data: Partial<ListPagination> },
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -90,10 +95,7 @@ export interface ListState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListTableProps = DataTableProps & {
|
export type ListTableProps = DataTableProps;
|
||||||
pagination: TablePagination;
|
|
||||||
scrollOffset: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PlaylistListFilter = Omit<PlaylistListArgs['query'], 'limit' | 'startIndex'>;
|
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 { DataTableProps } from '/@/renderer/store/settings.store';
|
||||||
import { mergeOverridingColumns } from '/@/renderer/store/utils';
|
import { mergeOverridingColumns } from '/@/renderer/store/utils';
|
||||||
import { PlaylistListSort, SortOrder } from '/@/shared/types/domain-types';
|
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 {
|
export interface PlaylistSlice extends PlaylistState {
|
||||||
actions: {
|
actions: {
|
||||||
@@ -17,11 +17,11 @@ export interface PlaylistSlice extends PlaylistState {
|
|||||||
setGrid: (args: { data: Partial<ListGridProps> }) => void;
|
setGrid: (args: { data: Partial<ListGridProps> }) => void;
|
||||||
setStore: (data: Partial<PlaylistSlice>) => void;
|
setStore: (data: Partial<PlaylistSlice>) => void;
|
||||||
setTable: (data: Partial<TableProps>) => 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;
|
scrollOffset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ interface PlaylistState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type TableProps = DataTableProps & {
|
type TableProps = DataTableProps & {
|
||||||
pagination: TablePagination;
|
pagination: ListPagination;
|
||||||
scrollOffset: number;
|
scrollOffset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user