Add album table view
This commit is contained in:
parent
e5ad41b9da
commit
b967c8cb19
5 changed files with 461 additions and 85 deletions
|
@ -34,6 +34,21 @@ export const SONG_TABLE_COLUMNS = [
|
||||||
// { label: 'Skip', value: TableColumn.SKIP },
|
// { label: 'Skip', value: TableColumn.SKIP },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const ALBUM_TABLE_COLUMNS = [
|
||||||
|
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
|
||||||
|
{ label: 'Title', value: TableColumn.TITLE },
|
||||||
|
{ label: 'Title (Combined)', value: TableColumn.TITLE_COMBINED },
|
||||||
|
{ label: 'Duration', value: TableColumn.DURATION },
|
||||||
|
{ label: 'Album Artist', value: TableColumn.ALBUM_ARTIST },
|
||||||
|
{ label: 'Artist', value: TableColumn.ARTIST },
|
||||||
|
{ label: 'Genre', value: TableColumn.GENRE },
|
||||||
|
{ label: 'Year', value: TableColumn.YEAR },
|
||||||
|
{ label: 'Release Date', value: TableColumn.RELEASE_DATE },
|
||||||
|
{ label: 'Last Played', value: TableColumn.LAST_PLAYED },
|
||||||
|
{ label: 'Date Added', value: TableColumn.DATE_ADDED },
|
||||||
|
{ label: 'Plays', value: TableColumn.PLAY_COUNT },
|
||||||
|
];
|
||||||
|
|
||||||
interface TableConfigDropdownProps {
|
interface TableConfigDropdownProps {
|
||||||
type: TableType;
|
type: TableType;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
import {
|
import {
|
||||||
ALBUM_CARD_ROWS,
|
ALBUM_CARD_ROWS,
|
||||||
|
getColumnDefs,
|
||||||
|
TablePagination,
|
||||||
VirtualGridAutoSizerContainer,
|
VirtualGridAutoSizerContainer,
|
||||||
VirtualInfiniteGrid,
|
VirtualInfiniteGrid,
|
||||||
VirtualInfiniteGridRef,
|
VirtualInfiniteGridRef,
|
||||||
|
VirtualTable,
|
||||||
} from '/@/renderer/components';
|
} from '/@/renderer/components';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { ListDisplayType, CardRow, LibraryItem } from '/@/renderer/types';
|
import { ListDisplayType, CardRow, LibraryItem } from '/@/renderer/types';
|
||||||
|
@ -16,25 +19,152 @@ import { Album, AlbumListSort } from '/@/renderer/api/types';
|
||||||
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-query';
|
||||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { useCurrentServer, useSetAlbumStore, useAlbumListStore } from '/@/renderer/store';
|
import {
|
||||||
|
useCurrentServer,
|
||||||
|
useSetAlbumStore,
|
||||||
|
useAlbumListStore,
|
||||||
|
useAlbumTablePagination,
|
||||||
|
useSetAlbumTable,
|
||||||
|
useSetAlbumTablePagination,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
|
import {
|
||||||
|
BodyScrollEvent,
|
||||||
|
ColDef,
|
||||||
|
GridReadyEvent,
|
||||||
|
IDatasource,
|
||||||
|
PaginationChangedEvent,
|
||||||
|
} from '@ag-grid-community/core';
|
||||||
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
|
|
||||||
interface AlbumListContentProps {
|
interface AlbumListContentProps {
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListContent = ({ gridRef }: AlbumListContentProps) => {
|
export const AlbumListContent = ({ gridRef, tableRef }: AlbumListContentProps) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const page = useAlbumListStore();
|
const page = useAlbumListStore();
|
||||||
const setPage = useSetAlbumStore();
|
const setPage = useSetAlbumStore();
|
||||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||||
|
|
||||||
const albumListQuery = useAlbumList({
|
const pagination = useAlbumTablePagination();
|
||||||
|
const setPagination = useSetAlbumTablePagination();
|
||||||
|
const setTable = useSetAlbumTable();
|
||||||
|
|
||||||
|
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
|
||||||
|
|
||||||
|
const checkAlbumList = useAlbumList({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
startIndex: 0,
|
startIndex: 0,
|
||||||
...page.filter,
|
...page.filter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const columnDefs: ColDef[] = useMemo(
|
||||||
|
() => getColumnDefs(page.table.columns),
|
||||||
|
[page.table.columns],
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultColumnDefs: ColDef = useMemo(() => {
|
||||||
|
return {
|
||||||
|
lockPinned: true,
|
||||||
|
lockVisible: true,
|
||||||
|
resizable: true,
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onTableReady = useCallback(
|
||||||
|
(params: GridReadyEvent) => {
|
||||||
|
const dataSource: IDatasource = {
|
||||||
|
getRows: async (params) => {
|
||||||
|
const limit = params.endRow - params.startRow;
|
||||||
|
const startIndex = params.startRow;
|
||||||
|
|
||||||
|
const queryKey = queryKeys.albums.list(server?.id || '', {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...page.filter,
|
||||||
|
});
|
||||||
|
|
||||||
|
const albumsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
|
||||||
|
api.controller.getAlbumList({
|
||||||
|
query: {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...page.filter,
|
||||||
|
},
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const albums = api.normalize.albumList(albumsRes, server);
|
||||||
|
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined);
|
||||||
|
},
|
||||||
|
rowCount: undefined,
|
||||||
|
};
|
||||||
|
params.api.setDatasource(dataSource);
|
||||||
|
// params.api.ensureIndexVisible(page.table.scrollOffset || 0, 'top');
|
||||||
|
},
|
||||||
|
[page.filter, queryClient, server],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onTablePaginationChanged = useCallback(
|
||||||
|
(event: PaginationChangedEvent) => {
|
||||||
|
if (!isPaginationEnabled || !event.api) return;
|
||||||
|
|
||||||
|
// Scroll to top of page on pagination change
|
||||||
|
const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage;
|
||||||
|
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
|
||||||
|
|
||||||
|
setPagination({
|
||||||
|
itemsPerPage: event.api.paginationGetPageSize(),
|
||||||
|
totalItems: event.api.paginationGetRowCount(),
|
||||||
|
totalPages: event.api.paginationGetTotalPages() + 1,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[isPaginationEnabled, pagination.currentPage, pagination.itemsPerPage, setPagination],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTableSizeChange = () => {
|
||||||
|
if (page.table.autoFit) {
|
||||||
|
tableRef?.current?.api.sizeColumnsToFit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableColumnChange = useCallback(() => {
|
||||||
|
const { columnApi } = tableRef?.current || {};
|
||||||
|
const columnsOrder = columnApi?.getAllGridColumns();
|
||||||
|
|
||||||
|
if (!columnsOrder) return;
|
||||||
|
|
||||||
|
const columnsInSettings = page.table.columns;
|
||||||
|
const updatedColumns = [];
|
||||||
|
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.getColDef().width,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTable({ columns: updatedColumns });
|
||||||
|
}, [page.table.autoFit, page.table.columns, setTable, tableRef]);
|
||||||
|
|
||||||
|
const debouncedTableColumnChange = debounce(handleTableColumnChange, 200);
|
||||||
|
|
||||||
|
const handleTableScroll = (e: BodyScrollEvent) => {
|
||||||
|
const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0));
|
||||||
|
setTable({ scrollOffset });
|
||||||
|
};
|
||||||
|
|
||||||
const fetch = useCallback(
|
const fetch = useCallback(
|
||||||
async ({ skip, take }: { skip: number; take: number }) => {
|
async ({ skip, take }: { skip: number; take: number }) => {
|
||||||
const queryKey = queryKeys.albums.list(server?.id || '', {
|
const queryKey = queryKeys.albums.list(server?.id || '', {
|
||||||
|
@ -139,10 +269,13 @@ export const AlbumListContent = ({ gridRef }: AlbumListContentProps) => {
|
||||||
}, [page.filter.sortBy]);
|
}, [page.filter.sortBy]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<VirtualGridAutoSizerContainer>
|
<VirtualGridAutoSizerContainer>
|
||||||
|
{page.display === ListDisplayType.CARD || page.display === ListDisplayType.POSTER ? (
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => (
|
{({ height, width }) => (
|
||||||
<VirtualInfiniteGrid
|
<VirtualInfiniteGrid
|
||||||
|
key={`album-list-${server?.id}-${page.display}`}
|
||||||
ref={gridRef}
|
ref={gridRef}
|
||||||
cardRows={cardRows}
|
cardRows={cardRows}
|
||||||
display={page.display || ListDisplayType.CARD}
|
display={page.display || ListDisplayType.CARD}
|
||||||
|
@ -150,7 +283,7 @@ export const AlbumListContent = ({ gridRef }: AlbumListContentProps) => {
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
height={height}
|
height={height}
|
||||||
initialScrollOffset={page?.grid.scrollOffset || 0}
|
initialScrollOffset={page?.grid.scrollOffset || 0}
|
||||||
itemCount={albumListQuery?.data?.totalRecordCount || 0}
|
itemCount={checkAlbumList?.data?.totalRecordCount || 0}
|
||||||
itemGap={20}
|
itemGap={20}
|
||||||
itemSize={150 + page.grid?.size}
|
itemSize={150 + page.grid?.size}
|
||||||
itemType={LibraryItem.ALBUM}
|
itemType={LibraryItem.ALBUM}
|
||||||
|
@ -164,6 +297,60 @@ export const AlbumListContent = ({ gridRef }: AlbumListContentProps) => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
) : (
|
||||||
|
<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-${page.display}-${page.table.rowHeight}-${server?.id}`}
|
||||||
|
ref={tableRef}
|
||||||
|
alwaysShowHorizontalScroll
|
||||||
|
animateRows
|
||||||
|
maintainColumnOrder
|
||||||
|
suppressCopyRowsToClipboard
|
||||||
|
suppressMoveWhenRowDragging
|
||||||
|
suppressPaginationPanel
|
||||||
|
suppressRowDrag
|
||||||
|
suppressScrollOnNewData
|
||||||
|
blockLoadDebounceMillis={200}
|
||||||
|
cacheBlockSize={500}
|
||||||
|
cacheOverflowSize={1}
|
||||||
|
columnDefs={columnDefs}
|
||||||
|
defaultColDef={defaultColumnDefs}
|
||||||
|
enableCellChangeFlash={false}
|
||||||
|
getRowId={(data) => data.data.id}
|
||||||
|
infiniteInitialRowCount={checkAlbumList.data?.totalRecordCount || 100}
|
||||||
|
pagination={isPaginationEnabled}
|
||||||
|
paginationAutoPageSize={isPaginationEnabled}
|
||||||
|
paginationPageSize={page.table.pagination.itemsPerPage || 100}
|
||||||
|
rowBuffer={20}
|
||||||
|
rowHeight={page.table.rowHeight || 40}
|
||||||
|
rowModelType="infinite"
|
||||||
|
rowSelection="multiple"
|
||||||
|
onBodyScrollEnd={handleTableScroll}
|
||||||
|
onCellContextMenu={(e) => console.log('context', e)}
|
||||||
|
onColumnMoved={handleTableColumnChange}
|
||||||
|
onColumnResized={debouncedTableColumnChange}
|
||||||
|
onGridReady={onTableReady}
|
||||||
|
onGridSizeChanged={handleTableSizeChange}
|
||||||
|
onPaginationChanged={onTablePaginationChanged}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</VirtualGridAutoSizerContainer>
|
</VirtualGridAutoSizerContainer>
|
||||||
|
{isPaginationEnabled && (
|
||||||
|
<AnimatePresence
|
||||||
|
presenceAffectsLayout
|
||||||
|
initial={false}
|
||||||
|
mode="wait"
|
||||||
|
>
|
||||||
|
{page.display === ListDisplayType.TABLE_PAGINATED && (
|
||||||
|
<TablePagination
|
||||||
|
pagination={pagination}
|
||||||
|
setPagination={setPagination}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { Flex, Slider } from '@mantine/core';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import throttle from 'lodash/throttle';
|
|
||||||
import type { ChangeEvent, MouseEvent, MutableRefObject } from 'react';
|
import type { ChangeEvent, MouseEvent, MutableRefObject } from 'react';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
import { IDatasource } from '@ag-grid-community/core';
|
||||||
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
|
import { Flex, Group, Stack } from '@mantine/core';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
import {
|
import {
|
||||||
RiArrowDownSLine,
|
RiArrowDownSLine,
|
||||||
RiFilter3Line,
|
RiFilter3Line,
|
||||||
|
@ -18,11 +19,16 @@ import { controller } from '/@/renderer/api/controller';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { AlbumListSort, ServerType, SortOrder } from '/@/renderer/api/types';
|
import { AlbumListSort, ServerType, SortOrder } from '/@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
|
ALBUM_TABLE_COLUMNS,
|
||||||
Button,
|
Button,
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
MultiSelect,
|
||||||
PageHeader,
|
PageHeader,
|
||||||
Popover,
|
Popover,
|
||||||
SearchInput,
|
SearchInput,
|
||||||
|
Slider,
|
||||||
|
Switch,
|
||||||
|
Text,
|
||||||
TextTitle,
|
TextTitle,
|
||||||
VirtualInfiniteGridRef,
|
VirtualInfiniteGridRef,
|
||||||
} from '/@/renderer/components';
|
} from '/@/renderer/components';
|
||||||
|
@ -36,8 +42,10 @@ import {
|
||||||
useCurrentServer,
|
useCurrentServer,
|
||||||
useSetAlbumFilters,
|
useSetAlbumFilters,
|
||||||
useSetAlbumStore,
|
useSetAlbumStore,
|
||||||
|
useSetAlbumTable,
|
||||||
|
useSetAlbumTablePagination,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { ListDisplayType } from '/@/renderer/types';
|
import { ListDisplayType, TableColumn } from '/@/renderer/types';
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
jellyfin: [
|
jellyfin: [
|
||||||
|
@ -82,9 +90,10 @@ const HeaderItems = styled.div`
|
||||||
|
|
||||||
interface AlbumListHeaderProps {
|
interface AlbumListHeaderProps {
|
||||||
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
|
||||||
|
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
export const AlbumListHeader = ({ gridRef, tableRef }: AlbumListHeaderProps) => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
const setPage = useSetAlbumStore();
|
const setPage = useSetAlbumStore();
|
||||||
|
@ -95,6 +104,9 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
|
|
||||||
const musicFoldersQuery = useMusicFolders();
|
const musicFoldersQuery = useMusicFolders();
|
||||||
|
|
||||||
|
const setPagination = useSetAlbumTablePagination();
|
||||||
|
const setTable = useSetAlbumTable();
|
||||||
|
|
||||||
const sortByLabel =
|
const sortByLabel =
|
||||||
(server?.type &&
|
(server?.type &&
|
||||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) ||
|
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) ||
|
||||||
|
@ -102,13 +114,16 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
|
|
||||||
const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown';
|
const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown';
|
||||||
|
|
||||||
const setSize = throttle(
|
const handleItemSize = (e: number) => {
|
||||||
(e: number) =>
|
if (
|
||||||
setPage({
|
page.display === ListDisplayType.TABLE ||
|
||||||
list: { ...page, grid: { ...page.grid, size: e } },
|
page.display === ListDisplayType.TABLE_PAGINATED
|
||||||
}),
|
) {
|
||||||
200,
|
setTable({ rowHeight: e });
|
||||||
);
|
} else {
|
||||||
|
setPage({ list: { ...page, grid: { ...page.grid, size: e } } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const fetch = useCallback(
|
const fetch = useCallback(
|
||||||
async (skip: number, take: number, filters: AlbumListFilter) => {
|
async (skip: number, take: number, filters: AlbumListFilter) => {
|
||||||
|
@ -137,6 +152,46 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
|
|
||||||
const handleFilterChange = useCallback(
|
const handleFilterChange = useCallback(
|
||||||
async (filters: AlbumListFilter) => {
|
async (filters: AlbumListFilter) => {
|
||||||
|
if (
|
||||||
|
page.display === ListDisplayType.TABLE ||
|
||||||
|
page.display === ListDisplayType.TABLE_PAGINATED
|
||||||
|
) {
|
||||||
|
const dataSource: IDatasource = {
|
||||||
|
getRows: async (params) => {
|
||||||
|
const limit = params.endRow - params.startRow;
|
||||||
|
const startIndex = params.startRow;
|
||||||
|
|
||||||
|
const queryKey = queryKeys.albums.list(server?.id || '', {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
const albumsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) =>
|
||||||
|
api.controller.getAlbumList({
|
||||||
|
query: {
|
||||||
|
limit,
|
||||||
|
startIndex,
|
||||||
|
...filters,
|
||||||
|
},
|
||||||
|
server,
|
||||||
|
signal,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const albums = api.normalize.albumList(albumsRes, server);
|
||||||
|
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined);
|
||||||
|
},
|
||||||
|
rowCount: undefined,
|
||||||
|
};
|
||||||
|
tableRef.current?.api.setDatasource(dataSource);
|
||||||
|
tableRef.current?.api.purgeInfiniteCache();
|
||||||
|
tableRef.current?.api.ensureIndexVisible(0, 'top');
|
||||||
|
|
||||||
|
if (page.display === ListDisplayType.TABLE_PAGINATED) {
|
||||||
|
setPagination({ currentPage: 0 });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
gridRef.current?.scrollTo(0);
|
gridRef.current?.scrollTo(0);
|
||||||
gridRef.current?.resetLoadMoreItemsCache();
|
gridRef.current?.resetLoadMoreItemsCache();
|
||||||
|
|
||||||
|
@ -147,8 +202,9 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
|
|
||||||
if (!data?.items) return;
|
if (!data?.items) return;
|
||||||
gridRef.current?.setItemData(data.items);
|
gridRef.current?.setItemData(data.items);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[gridRef, fetch],
|
[page.display, tableRef, setPagination, server, queryClient, gridRef, fetch],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSetSortBy = useCallback(
|
const handleSetSortBy = useCallback(
|
||||||
|
@ -194,14 +250,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
const handleSetViewType = useCallback(
|
const handleSetViewType = useCallback(
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
(e: MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!e.currentTarget?.value) return;
|
if (!e.currentTarget?.value) return;
|
||||||
const type = e.currentTarget.value;
|
setPage({ list: { ...page, display: e.currentTarget.value as ListDisplayType } });
|
||||||
if (type === ListDisplayType.CARD) {
|
|
||||||
setPage({ list: { ...page, display: ListDisplayType.CARD } });
|
|
||||||
} else if (type === ListDisplayType.POSTER) {
|
|
||||||
setPage({ list: { ...page, display: ListDisplayType.POSTER } });
|
|
||||||
} else {
|
|
||||||
setPage({ list: { ...page, display: ListDisplayType.TABLE } });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[page, setPage],
|
[page, setPage],
|
||||||
);
|
);
|
||||||
|
@ -213,6 +262,39 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters);
|
if (previousSearchTerm !== searchTerm) handleFilterChange(updatedFilters);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
const handleTableColumns = (values: TableColumn[]) => {
|
||||||
|
const existingColumns = page.table.columns;
|
||||||
|
|
||||||
|
if (values.length === 0) {
|
||||||
|
return setTable({
|
||||||
|
columns: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If adding a column
|
||||||
|
if (values.length > existingColumns.length) {
|
||||||
|
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||||
|
|
||||||
|
setTable({ columns: [...existingColumns, newColumn] });
|
||||||
|
} else {
|
||||||
|
// If removing a column
|
||||||
|
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
||||||
|
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
||||||
|
|
||||||
|
setTable({ columns: newColumns });
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableRef.current?.api.sizeColumnsToFit();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setTable({ autoFit: e.currentTarget.checked });
|
||||||
|
|
||||||
|
if (e.currentTarget.checked) {
|
||||||
|
tableRef.current?.api.sizeColumnsToFit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
<HeaderItems ref={cq.ref}>
|
<HeaderItems ref={cq.ref}>
|
||||||
|
@ -239,15 +321,6 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenu.Target>
|
</DropdownMenu.Target>
|
||||||
<DropdownMenu.Dropdown>
|
<DropdownMenu.Dropdown>
|
||||||
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
|
||||||
<DropdownMenu.Item>
|
|
||||||
<Slider
|
|
||||||
defaultValue={page.grid.size || 0}
|
|
||||||
label={null}
|
|
||||||
onChange={setSize}
|
|
||||||
/>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Divider />
|
|
||||||
<DropdownMenu.Label>Display type</DropdownMenu.Label>
|
<DropdownMenu.Label>Display type</DropdownMenu.Label>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
$isActive={page.display === ListDisplayType.CARD}
|
$isActive={page.display === ListDisplayType.CARD}
|
||||||
|
@ -264,20 +337,69 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
Poster
|
Poster
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
disabled
|
|
||||||
$isActive={page.display === ListDisplayType.TABLE}
|
$isActive={page.display === ListDisplayType.TABLE}
|
||||||
value="list"
|
value={ListDisplayType.TABLE}
|
||||||
onClick={handleSetViewType}
|
onClick={handleSetViewType}
|
||||||
>
|
>
|
||||||
List
|
Table
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
$isActive={page.display === ListDisplayType.TABLE_PAGINATED}
|
||||||
|
value={ListDisplayType.TABLE_PAGINATED}
|
||||||
|
onClick={handleSetViewType}
|
||||||
|
>
|
||||||
|
Table (paginated)
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
<DropdownMenu.Divider />
|
||||||
|
<DropdownMenu.Label>Item size</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||||
|
<Slider
|
||||||
|
defaultValue={
|
||||||
|
page.display === ListDisplayType.CARD || page.display === ListDisplayType.POSTER
|
||||||
|
? page.grid.size
|
||||||
|
: page.table.rowHeight
|
||||||
|
}
|
||||||
|
label={null}
|
||||||
|
max={100}
|
||||||
|
min={25}
|
||||||
|
onChangeEnd={handleItemSize}
|
||||||
|
/>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
{(page.display === ListDisplayType.TABLE ||
|
||||||
|
page.display === ListDisplayType.TABLE_PAGINATED) && (
|
||||||
|
<>
|
||||||
|
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
closeMenuOnClick={false}
|
||||||
|
component="div"
|
||||||
|
sx={{ cursor: 'default' }}
|
||||||
|
>
|
||||||
|
<Stack>
|
||||||
|
<MultiSelect
|
||||||
|
clearable
|
||||||
|
data={ALBUM_TABLE_COLUMNS}
|
||||||
|
defaultValue={page.table?.columns.map((column) => column.column)}
|
||||||
|
width={300}
|
||||||
|
onChange={handleTableColumns}
|
||||||
|
/>
|
||||||
|
<Group position="apart">
|
||||||
|
<Text>Auto Fit Columns</Text>
|
||||||
|
<Switch
|
||||||
|
defaultChecked={page.table.autoFit}
|
||||||
|
onChange={handleAutoFitColumns}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<DropdownMenu position="bottom-start">
|
<DropdownMenu position="bottom-start">
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
fw="normal"
|
fw="600"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{sortByLabel}
|
{sortByLabel}
|
||||||
|
@ -298,8 +420,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
fw="normal"
|
fw="600"
|
||||||
tooltip={!cq.isMd ? { label: sortOrderLabel } : undefined}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
onClick={handleToggleSortOrder}
|
onClick={handleToggleSortOrder}
|
||||||
>
|
>
|
||||||
|
@ -320,8 +441,7 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
fw="normal"
|
fw="600"
|
||||||
tooltip={!cq.isMd ? { label: 'Folder' } : undefined}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
|
{cq.isMd ? 'Folder' : <RiFolder2Line size={15} />}
|
||||||
|
@ -341,15 +461,11 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
</DropdownMenu.Dropdown>
|
</DropdownMenu.Dropdown>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
<Popover
|
<Popover position="bottom-start">
|
||||||
closeOnClickOutside={false}
|
|
||||||
position="bottom-start"
|
|
||||||
>
|
|
||||||
<Popover.Target>
|
<Popover.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
fw="normal"
|
fw="600"
|
||||||
tooltip={!cq.isMd ? { label: 'Filters' } : undefined}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
{cq.isMd ? 'Filters' : <RiFilter3Line size={15} />}
|
{cq.isMd ? 'Filters' : <RiFilter3Line size={15} />}
|
||||||
|
@ -367,7 +483,6 @@ export const AlbumListHeader = ({ gridRef }: AlbumListHeaderProps) => {
|
||||||
<DropdownMenu.Target>
|
<DropdownMenu.Target>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
tooltip={{ label: 'More' }}
|
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
>
|
>
|
||||||
<RiMoreFill size={15} />
|
<RiMoreFill size={15} />
|
||||||
|
|
|
@ -3,15 +3,23 @@ import { AnimatedPage } from '/@/renderer/features/shared';
|
||||||
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
|
||||||
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
|
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||||
|
|
||||||
const AlbumListRoute = () => {
|
const AlbumListRoute = () => {
|
||||||
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
const gridRef = useRef<VirtualInfiniteGridRef | null>(null);
|
||||||
|
const tableRef = useRef<AgGridReactType | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<VirtualGridContainer>
|
<VirtualGridContainer>
|
||||||
<AlbumListHeader gridRef={gridRef} />
|
<AlbumListHeader
|
||||||
<AlbumListContent gridRef={gridRef} />
|
gridRef={gridRef}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
|
<AlbumListContent
|
||||||
|
gridRef={gridRef}
|
||||||
|
tableRef={tableRef}
|
||||||
|
/>
|
||||||
</VirtualGridContainer>
|
</VirtualGridContainer>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,11 +3,13 @@ import create from 'zustand';
|
||||||
import { devtools, persist } from 'zustand/middleware';
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
import { immer } from 'zustand/middleware/immer';
|
import { immer } from 'zustand/middleware/immer';
|
||||||
import { AlbumListArgs, AlbumListSort, SortOrder } from '/@/renderer/api/types';
|
import { AlbumListArgs, AlbumListSort, SortOrder } from '/@/renderer/api/types';
|
||||||
import { ListDisplayType } from '/@/renderer/types';
|
import { DataTableProps } from '/@/renderer/store/settings.store';
|
||||||
|
import { ListDisplayType, TableColumn, TablePagination } from '/@/renderer/types';
|
||||||
|
|
||||||
type TableProps = {
|
type TableProps = {
|
||||||
|
pagination: TablePagination;
|
||||||
scrollOffset: number;
|
scrollOffset: number;
|
||||||
};
|
} & DataTableProps;
|
||||||
|
|
||||||
type ListProps<T> = {
|
type ListProps<T> = {
|
||||||
display: ListDisplayType;
|
display: ListDisplayType;
|
||||||
|
@ -29,6 +31,8 @@ export interface AlbumSlice extends AlbumState {
|
||||||
actions: {
|
actions: {
|
||||||
setFilters: (data: Partial<AlbumListFilter>) => AlbumListFilter;
|
setFilters: (data: Partial<AlbumListFilter>) => AlbumListFilter;
|
||||||
setStore: (data: Partial<AlbumSlice>) => void;
|
setStore: (data: Partial<AlbumSlice>) => void;
|
||||||
|
setTable: (data: Partial<TableProps>) => void;
|
||||||
|
setTablePagination: (data: Partial<TableProps['pagination']>) => void;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,6 +51,16 @@ export const useAlbumStore = create<AlbumSlice>()(
|
||||||
setStore: (data) => {
|
setStore: (data) => {
|
||||||
set({ ...get(), ...data });
|
set({ ...get(), ...data });
|
||||||
},
|
},
|
||||||
|
setTable: (data) => {
|
||||||
|
set((state) => {
|
||||||
|
state.list.table = { ...state.list.table, ...data };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setTablePagination: (data) => {
|
||||||
|
set((state) => {
|
||||||
|
state.list.table.pagination = { ...state.list.table.pagination, ...data };
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
list: {
|
list: {
|
||||||
display: ListDisplayType.CARD,
|
display: ListDisplayType.CARD,
|
||||||
|
@ -60,6 +74,36 @@ export const useAlbumStore = create<AlbumSlice>()(
|
||||||
size: 50,
|
size: 50,
|
||||||
},
|
},
|
||||||
table: {
|
table: {
|
||||||
|
autoFit: true,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
column: TableColumn.ROW_INDEX,
|
||||||
|
width: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.TITLE_COMBINED,
|
||||||
|
width: 500,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.DURATION,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.ALBUM_ARTIST,
|
||||||
|
width: 300,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
column: TableColumn.YEAR,
|
||||||
|
width: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pagination: {
|
||||||
|
currentPage: 1,
|
||||||
|
itemsPerPage: 100,
|
||||||
|
totalItems: 1,
|
||||||
|
totalPages: 1,
|
||||||
|
},
|
||||||
|
rowHeight: 60,
|
||||||
scrollOffset: 0,
|
scrollOffset: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -83,3 +127,10 @@ export const useSetAlbumStore = () => useAlbumStore((state) => state.actions.set
|
||||||
export const useSetAlbumFilters = () => useAlbumStore((state) => state.actions.setFilters);
|
export const useSetAlbumFilters = () => useAlbumStore((state) => state.actions.setFilters);
|
||||||
|
|
||||||
export const useAlbumListStore = () => useAlbumStore((state) => state.list);
|
export const useAlbumListStore = () => useAlbumStore((state) => state.list);
|
||||||
|
|
||||||
|
export const useAlbumTablePagination = () => useAlbumStore((state) => state.list.table.pagination);
|
||||||
|
|
||||||
|
export const useSetAlbumTablePagination = () =>
|
||||||
|
useAlbumStore((state) => state.actions.setTablePagination);
|
||||||
|
|
||||||
|
export const useSetAlbumTable = () => useAlbumStore((state) => state.actions.setTable);
|
||||||
|
|
Reference in a new issue