mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
Add filter functionality for infinite album list
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Card } from '@mantine/core';
|
import { Card, Skeleton } from '@mantine/core';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { CardRow } from 'renderer/types';
|
import { CardRow } from 'renderer/types';
|
||||||
@@ -10,7 +10,6 @@ const CardWrapper = styled(motion.div)<{
|
|||||||
itemHeight: number;
|
itemHeight: number;
|
||||||
itemWidth: number;
|
itemWidth: number;
|
||||||
}>`
|
}>`
|
||||||
display: flex;
|
|
||||||
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
|
flex: ${({ itemWidth }) => `0 0 ${itemWidth}px`};
|
||||||
width: ${({ itemWidth }) => `${itemWidth}px`};
|
width: ${({ itemWidth }) => `${itemWidth}px`};
|
||||||
height: ${({ itemHeight }) => `${itemHeight}px`};
|
height: ${({ itemHeight }) => `${itemHeight}px`};
|
||||||
@@ -46,9 +45,17 @@ const ImageSection = styled.div`
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled(motion.div)<{ height: number; src: string }>`
|
interface ImageProps {
|
||||||
|
height: number;
|
||||||
|
src: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image = styled(motion.div).attrs((props: ImageProps) => ({
|
||||||
|
style: {
|
||||||
|
background: `url(${props.src})`,
|
||||||
|
},
|
||||||
|
}))<ImageProps>`
|
||||||
height: ${({ height }) => `${height}px`};
|
height: ${({ height }) => `${height}px`};
|
||||||
background: ${({ src }) => `url(${src})`};
|
|
||||||
background-position: center;
|
background-position: center;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border: 0;
|
border: 0;
|
||||||
@@ -82,9 +89,9 @@ export const GridCard = ({ data, index, style, isScrolling }: any) => {
|
|||||||
itemGap,
|
itemGap,
|
||||||
itemCount,
|
itemCount,
|
||||||
cardControls,
|
cardControls,
|
||||||
|
handlePlayQueueAdd,
|
||||||
cardRows,
|
cardRows,
|
||||||
itemData,
|
itemData,
|
||||||
handlePlayQueueAdd,
|
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const startIndex = index * columnCount;
|
const startIndex = index * columnCount;
|
||||||
@@ -98,45 +105,33 @@ export const GridCard = ({ data, index, style, isScrolling }: any) => {
|
|||||||
itemGap={itemGap}
|
itemGap={itemGap}
|
||||||
itemHeight={itemHeight}
|
itemHeight={itemHeight}
|
||||||
itemWidth={itemWidth}
|
itemWidth={itemWidth}
|
||||||
tabIndex={0}
|
|
||||||
>
|
>
|
||||||
<StyledCard>
|
<Skeleton visible={!itemData[i]}>
|
||||||
<ImageSection>
|
<StyledCard>
|
||||||
<Image
|
<ImageSection>
|
||||||
animate={{
|
<Image height={itemWidth} src={itemData[i]?.image}>
|
||||||
opacity: 1,
|
|
||||||
}}
|
|
||||||
height={itemWidth}
|
|
||||||
initial={{
|
|
||||||
opacity: 0,
|
|
||||||
}}
|
|
||||||
src={itemData[i]?.image}
|
|
||||||
transition={{
|
|
||||||
duration: 0.5,
|
|
||||||
ease: 'anticipate',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{!isScrolling && (
|
|
||||||
<ControlsContainer>
|
<ControlsContainer>
|
||||||
<GridCardControls
|
{!isScrolling && (
|
||||||
cardControls={cardControls}
|
<GridCardControls
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
cardControls={cardControls}
|
||||||
itemData={itemData[i]}
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
/>
|
itemData={itemData[i]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ControlsContainer>
|
</ControlsContainer>
|
||||||
)}
|
</Image>
|
||||||
</Image>
|
</ImageSection>
|
||||||
</ImageSection>
|
<DetailSection>
|
||||||
<DetailSection>
|
{cardRows.map((row: CardRow) => (
|
||||||
{cardRows.map((row: CardRow) => (
|
<Row key={`row-${row.prop}`}>
|
||||||
<Row key={`row-${row.prop}`}>
|
<Text overflow="hidden" weight={500}>
|
||||||
<Text overflow="hidden" weight={500}>
|
{itemData[i] && itemData[i][row.prop]}
|
||||||
{itemData[i] && itemData[i][row.prop]}
|
</Text>
|
||||||
</Text>
|
</Row>
|
||||||
</Row>
|
))}
|
||||||
))}
|
</DetailSection>
|
||||||
</DetailSection>
|
</StyledCard>
|
||||||
</StyledCard>
|
</Skeleton>
|
||||||
</CardWrapper>
|
</CardWrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ export const VirtualGridWrapper = ({
|
|||||||
itemCount,
|
itemCount,
|
||||||
columnCount,
|
columnCount,
|
||||||
rowCount,
|
rowCount,
|
||||||
|
itemData,
|
||||||
...rest
|
...rest
|
||||||
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
|
}: Omit<FixedSizeListProps, 'ref' | 'itemSize' | 'children'> & {
|
||||||
cardControls: any;
|
cardControls: any;
|
||||||
cardRows: CardRow[];
|
cardRows: CardRow[];
|
||||||
columnCount: number;
|
columnCount: number;
|
||||||
|
itemData: any[];
|
||||||
itemGap: number;
|
itemGap: number;
|
||||||
itemHeight: number;
|
itemHeight: number;
|
||||||
itemWidth: number;
|
itemWidth: number;
|
||||||
@@ -27,39 +29,37 @@ export const VirtualGridWrapper = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { handlePlayQueueAdd } = usePlayQueueHandler();
|
const { handlePlayQueueAdd } = usePlayQueueHandler();
|
||||||
|
|
||||||
const itemData = useMemo(
|
const memo = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
cardControls,
|
cardControls,
|
||||||
cardRows,
|
cardRows,
|
||||||
columnCount,
|
columnCount,
|
||||||
handlePlayQueueAdd,
|
handlePlayQueueAdd,
|
||||||
itemCount,
|
itemCount,
|
||||||
itemData: rest.itemData,
|
itemData,
|
||||||
itemGap,
|
itemGap,
|
||||||
itemHeight,
|
itemHeight,
|
||||||
itemWidth,
|
itemWidth,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
cardRows,
|
|
||||||
cardControls,
|
cardControls,
|
||||||
|
cardRows,
|
||||||
columnCount,
|
columnCount,
|
||||||
|
handlePlayQueueAdd,
|
||||||
itemCount,
|
itemCount,
|
||||||
rest.itemData,
|
itemData,
|
||||||
itemGap,
|
itemGap,
|
||||||
itemHeight,
|
itemHeight,
|
||||||
itemWidth,
|
itemWidth,
|
||||||
handlePlayQueueAdd,
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
style={{ scrollBehavior: 'smooth' }}
|
|
||||||
{...rest}
|
|
||||||
ref={refInstance}
|
ref={refInstance}
|
||||||
initialScrollOffset={0}
|
{...rest}
|
||||||
itemCount={rowCount}
|
itemCount={rowCount}
|
||||||
itemData={itemData}
|
itemData={memo}
|
||||||
itemSize={itemHeight + itemGap}
|
itemSize={itemHeight + itemGap}
|
||||||
overscanCount={10}
|
overscanCount={10}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { forwardRef, Ref, useState } from 'react';
|
import { useState, useEffect, useRef, useMemo } from 'react';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
||||||
import { FixedSizeListProps } from 'react-window';
|
import { FixedSizeListProps } from 'react-window';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
import { CardRow } from 'renderer/types';
|
import { CardRow } from 'renderer/types';
|
||||||
import { VirtualGridWrapper } from './VirtualGridWrapper';
|
import { VirtualGridWrapper } from './VirtualGridWrapper';
|
||||||
|
|
||||||
interface VirtualGridProps
|
interface VirtualGridProps
|
||||||
extends Omit<
|
extends Omit<FixedSizeListProps, 'children' | 'itemSize'> {
|
||||||
FixedSizeListProps,
|
|
||||||
'children' | 'itemSize' | 'height' | 'width'
|
|
||||||
> {
|
|
||||||
cardControls: any;
|
cardControls: any;
|
||||||
cardRows: CardRow[];
|
cardRows: CardRow[];
|
||||||
itemGap?: number;
|
itemGap?: number;
|
||||||
@@ -20,119 +16,113 @@ interface VirtualGridProps
|
|||||||
queryParams?: Record<string, any>;
|
queryParams?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VirtualInfiniteGrid = forwardRef(
|
export const VirtualInfiniteGrid = ({
|
||||||
(
|
itemCount,
|
||||||
{
|
itemGap,
|
||||||
itemCount,
|
itemSize,
|
||||||
itemGap,
|
cardControls,
|
||||||
itemSize,
|
cardRows,
|
||||||
cardControls,
|
minimumBatchSize,
|
||||||
cardRows,
|
query,
|
||||||
minimumBatchSize,
|
queryParams,
|
||||||
query,
|
height,
|
||||||
queryParams,
|
width,
|
||||||
}: VirtualGridProps,
|
}: VirtualGridProps) => {
|
||||||
ref: Ref<InfiniteLoader>
|
const [itemData, setItemData] = useState<any[]>([]);
|
||||||
) => {
|
const listRef = useRef<any>(null);
|
||||||
const [itemData, setItemData] = useState<any[]>([]);
|
const loader = useRef<InfiniteLoader>(null);
|
||||||
|
|
||||||
const isItemLoaded = (index: number, columnCount: number) => {
|
const { itemHeight, rowCount, columnCount } = useMemo(() => {
|
||||||
const itemIndex = index * columnCount;
|
const itemsPerRow = Math.floor(
|
||||||
|
(Number(width) - itemGap! + 3) / (itemSize! + itemGap! + 2)
|
||||||
return (
|
|
||||||
itemIndex < itemData.length * columnCount &&
|
|
||||||
itemData[itemIndex] !== undefined
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadMoreItems = async (
|
|
||||||
startIndex: number,
|
|
||||||
stopIndex: number,
|
|
||||||
limit: number,
|
|
||||||
columnCount: number
|
|
||||||
) => {
|
|
||||||
const currentPage = Math.ceil(startIndex / minimumBatchSize!);
|
|
||||||
|
|
||||||
const t = await query({
|
|
||||||
limit,
|
|
||||||
page: currentPage,
|
|
||||||
...queryParams,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Need to multiply by columnCount due to the grid layout
|
|
||||||
const start = startIndex * columnCount;
|
|
||||||
const end = (stopIndex + 1) * columnCount;
|
|
||||||
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
const newData: any[] = [...itemData];
|
|
||||||
|
|
||||||
let itemIndex = 0;
|
|
||||||
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
|
|
||||||
newData[rowIndex] = t?.data[itemIndex];
|
|
||||||
itemIndex += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
setItemData(newData);
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedLoadMoreItems = debounce(loadMoreItems, 300);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AutoSizer>
|
|
||||||
{({ height, width }) => {
|
|
||||||
const itemHeight = itemSize! + cardRows.length * 25;
|
|
||||||
|
|
||||||
const columnCount = Math.floor(
|
|
||||||
(Number(width) - itemGap! + 3) / (itemSize! + itemGap! + 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
const rowCount = Math.ceil(itemCount / columnCount);
|
|
||||||
|
|
||||||
const pageItemLimit = columnCount * minimumBatchSize!;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InfiniteLoader
|
|
||||||
ref={ref}
|
|
||||||
isItemLoaded={(index) => isItemLoaded(index, columnCount)}
|
|
||||||
itemCount={itemCount || 0}
|
|
||||||
loadMoreItems={(startIndex, stopIndex) =>
|
|
||||||
debouncedLoadMoreItems(
|
|
||||||
startIndex,
|
|
||||||
stopIndex,
|
|
||||||
pageItemLimit,
|
|
||||||
columnCount
|
|
||||||
)
|
|
||||||
}
|
|
||||||
minimumBatchSize={minimumBatchSize}
|
|
||||||
threshold={10}
|
|
||||||
>
|
|
||||||
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
|
|
||||||
<VirtualGridWrapper
|
|
||||||
useIsScrolling
|
|
||||||
cardControls={cardControls}
|
|
||||||
cardRows={cardRows}
|
|
||||||
columnCount={columnCount}
|
|
||||||
height={height}
|
|
||||||
itemCount={itemCount || 0}
|
|
||||||
itemData={itemData}
|
|
||||||
itemGap={itemGap!}
|
|
||||||
itemHeight={itemHeight!}
|
|
||||||
itemWidth={itemSize}
|
|
||||||
refInstance={infiniteLoaderRef}
|
|
||||||
rowCount={rowCount}
|
|
||||||
width={width}
|
|
||||||
onItemsRendered={onItemsRendered}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</InfiniteLoader>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
);
|
return {
|
||||||
|
columnCount: itemsPerRow,
|
||||||
|
itemHeight: itemSize! + cardRows.length * 25,
|
||||||
|
rowCount: Math.ceil(itemCount / itemsPerRow),
|
||||||
|
};
|
||||||
|
}, [cardRows.length, itemCount, itemGap, itemSize, width]);
|
||||||
|
|
||||||
|
const isItemLoaded = (index: number) => {
|
||||||
|
const itemIndex = index * columnCount;
|
||||||
|
|
||||||
|
return itemData[itemIndex] !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMoreItems = async (startIndex: number, stopIndex: number) => {
|
||||||
|
// Fixes a caching bug(?) when switching between filters and the itemCount increases
|
||||||
|
if (startIndex === 1) return;
|
||||||
|
|
||||||
|
// Need to multiply by columnCount due to the grid layout
|
||||||
|
const start = startIndex * columnCount;
|
||||||
|
const end = stopIndex * columnCount + columnCount;
|
||||||
|
|
||||||
|
const t = await query({
|
||||||
|
limit: end - start,
|
||||||
|
skip: start,
|
||||||
|
...queryParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newData: any[] = [...itemData];
|
||||||
|
|
||||||
|
let itemIndex = 0;
|
||||||
|
for (let rowIndex = start; rowIndex < end; rowIndex += 1) {
|
||||||
|
newData[rowIndex] = t.data[itemIndex];
|
||||||
|
itemIndex += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
setItemData(newData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const debouncedLoadMoreItems = debounce(loadMoreItems, 300);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loader.current) {
|
||||||
|
listRef.current.scrollTo(0);
|
||||||
|
loader.current.resetloadMoreItemsCache(true);
|
||||||
|
setItemData(() => []);
|
||||||
|
|
||||||
|
loadMoreItems(0, minimumBatchSize! * 2);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [minimumBatchSize, queryParams, setItemData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteLoader
|
||||||
|
ref={loader}
|
||||||
|
isItemLoaded={(index) => isItemLoaded(index)}
|
||||||
|
itemCount={itemCount || 0}
|
||||||
|
loadMoreItems={(startIndex, stopIndex) =>
|
||||||
|
debouncedLoadMoreItems(startIndex, stopIndex)
|
||||||
|
}
|
||||||
|
minimumBatchSize={minimumBatchSize}
|
||||||
|
threshold={30}
|
||||||
|
>
|
||||||
|
{({ onItemsRendered, ref: infiniteLoaderRef }) => (
|
||||||
|
<VirtualGridWrapper
|
||||||
|
useIsScrolling
|
||||||
|
cardControls={cardControls}
|
||||||
|
cardRows={cardRows}
|
||||||
|
columnCount={columnCount}
|
||||||
|
height={height}
|
||||||
|
itemCount={itemCount || 0}
|
||||||
|
itemData={itemData}
|
||||||
|
itemGap={itemGap!}
|
||||||
|
itemHeight={itemHeight!}
|
||||||
|
itemWidth={itemSize}
|
||||||
|
refInstance={(list) => {
|
||||||
|
infiniteLoaderRef(list);
|
||||||
|
listRef.current = list;
|
||||||
|
}}
|
||||||
|
rowCount={rowCount}
|
||||||
|
width={width}
|
||||||
|
onItemsRendered={onItemsRendered}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</InfiniteLoader>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
VirtualInfiniteGrid.defaultProps = {
|
VirtualInfiniteGrid.defaultProps = {
|
||||||
itemGap: 10,
|
itemGap: 10,
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Dispatch } from 'react';
|
||||||
|
import { ActionIcon, Menu, MenuProps } from '@mantine/core';
|
||||||
|
import { LayoutGrid, LayoutList, Table } from 'tabler-icons-react';
|
||||||
|
|
||||||
|
export enum ViewType {
|
||||||
|
Detail = 'detail',
|
||||||
|
Grid = 'grid',
|
||||||
|
Table = 'table',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewTypeButtonProps {
|
||||||
|
handler: Dispatch<ViewType>;
|
||||||
|
menuProps: MenuProps;
|
||||||
|
type: ViewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ViewTypeButton = ({
|
||||||
|
type,
|
||||||
|
menuProps,
|
||||||
|
handler,
|
||||||
|
}: ViewTypeButtonProps) => {
|
||||||
|
return (
|
||||||
|
<Menu {...menuProps}>
|
||||||
|
<Menu.Target>
|
||||||
|
<ActionIcon variant="transparent">
|
||||||
|
{type === ViewType.Grid ? (
|
||||||
|
<LayoutGrid />
|
||||||
|
) : type === ViewType.Detail ? (
|
||||||
|
<LayoutList />
|
||||||
|
) : (
|
||||||
|
<Table />
|
||||||
|
)}
|
||||||
|
</ActionIcon>
|
||||||
|
</Menu.Target>
|
||||||
|
<Menu.Dropdown>
|
||||||
|
<Menu.Item
|
||||||
|
icon={<LayoutGrid size={14} />}
|
||||||
|
onClick={() => handler(ViewType.Grid)}
|
||||||
|
>
|
||||||
|
Grid
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
icon={<LayoutList size={14} />}
|
||||||
|
onClick={() => handler(ViewType.Detail)}
|
||||||
|
>
|
||||||
|
Detail
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
icon={<Table size={14} />}
|
||||||
|
onClick={() => handler(ViewType.Table)}
|
||||||
|
>
|
||||||
|
Table
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import { useInfiniteQuery, useQuery } from 'react-query';
|
import { useInfiniteQuery, useQuery } from 'react-query';
|
||||||
import { queryKeys } from 'renderer/api/queryKeys';
|
import { queryKeys } from 'renderer/api/queryKeys';
|
||||||
|
import { AlbumsResponse } from 'renderer/api/types';
|
||||||
import { albumsApi, AlbumsRequest } from '../../../api/albumsApi';
|
import { albumsApi, AlbumsRequest } from '../../../api/albumsApi';
|
||||||
|
|
||||||
export const useAlbums = (params: AlbumsRequest) => {
|
export const useAlbums = (params: AlbumsRequest) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryFn: () => albumsApi.getAlbums(params),
|
queryFn: () => albumsApi.getAlbums(params),
|
||||||
queryKey: queryKeys.albums(),
|
queryKey: queryKeys.albums(params),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useAlbumsInfinite = (params: AlbumsRequest) => {
|
export const useAlbumsInfinite = (params: AlbumsRequest) => {
|
||||||
return useInfiniteQuery({
|
return useInfiniteQuery({
|
||||||
getNextPageParam: (lastPage) => {
|
getNextPageParam: (lastPage: AlbumsResponse) => {
|
||||||
return !!lastPage.pagination.nextPage;
|
return !!lastPage.pagination.nextPage;
|
||||||
},
|
},
|
||||||
getPreviousPageParam: (firstPage) => {
|
getPreviousPageParam: (firstPage: AlbumsResponse) => {
|
||||||
return !!firstPage.pagination.prevPage;
|
return !!firstPage.pagination.prevPage;
|
||||||
},
|
},
|
||||||
queryFn: ({ pageParam }) =>
|
queryFn: ({ pageParam }) =>
|
||||||
|
|||||||
@@ -1,58 +1,155 @@
|
|||||||
/* eslint-disable no-plusplus */
|
/* eslint-disable no-plusplus */
|
||||||
import { useRef } from 'react';
|
import { useState } from 'react';
|
||||||
import InfiniteLoader from 'react-window-infinite-loader';
|
import { Button, Group, Menu } from '@mantine/core';
|
||||||
|
import { useSetState } from '@mantine/hooks';
|
||||||
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { CaretDown } from 'tabler-icons-react';
|
||||||
|
import i18n from 'i18n/i18n';
|
||||||
import { albumsApi } from 'renderer/api/albumsApi';
|
import { albumsApi } from 'renderer/api/albumsApi';
|
||||||
import { VirtualInfiniteGrid } from 'renderer/components/virtual-grid/VirtualInfiniteGrid';
|
import { VirtualInfiniteGrid } from 'renderer/components/virtual-grid/VirtualInfiniteGrid';
|
||||||
import { AnimatedPage } from 'renderer/features/shared/components/AnimatedPage';
|
import { AnimatedPage } from 'renderer/features/shared/components/AnimatedPage';
|
||||||
import { AppRoute } from 'renderer/router/utils/routes';
|
import { AppRoute } from 'renderer/router/utils/routes';
|
||||||
import { Item } from 'types';
|
import { Item } from 'types';
|
||||||
|
import { ViewType, ViewTypeButton } from '../components/ViewTypeButton';
|
||||||
import { useAlbums } from '../queries/getAlbums';
|
import { useAlbums } from '../queries/getAlbums';
|
||||||
|
|
||||||
export const LibraryAlbumsRoute = () => {
|
export enum AlbumSort {
|
||||||
const infiniteLoaderRef = useRef<InfiniteLoader>(null);
|
DATE_ADDED = 'date_added',
|
||||||
|
DATE_ADDED_REMOTE = 'date_added_remote',
|
||||||
|
DATE_PLAYED = 'date_played',
|
||||||
|
DATE_RELEASED = 'date_released',
|
||||||
|
RANDOM = 'random',
|
||||||
|
RATING = 'rating',
|
||||||
|
TITLE = 'title',
|
||||||
|
YEAR = 'year',
|
||||||
|
}
|
||||||
|
|
||||||
const params = {
|
const FILTERS = [
|
||||||
|
{ name: i18n.t('filters.dateAdded'), value: AlbumSort.DATE_ADDED },
|
||||||
|
{
|
||||||
|
name: i18n.t('filters.dateAddedRemote'),
|
||||||
|
value: AlbumSort.DATE_ADDED_REMOTE,
|
||||||
|
},
|
||||||
|
{ name: i18n.t('filters.datePlayed'), value: AlbumSort.DATE_PLAYED },
|
||||||
|
{ name: i18n.t('filters.dateReleased'), value: AlbumSort.DATE_RELEASED },
|
||||||
|
{ name: i18n.t('filters.random'), value: AlbumSort.RANDOM },
|
||||||
|
{ name: i18n.t('filters.rating'), value: AlbumSort.RATING },
|
||||||
|
{ name: i18n.t('filters.title'), value: AlbumSort.TITLE },
|
||||||
|
{ name: i18n.t('filters.year'), value: AlbumSort.YEAR },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const LibraryAlbumsRoute = () => {
|
||||||
|
const [viewType, setViewType] = useState(ViewType.Grid);
|
||||||
|
const [filters, setFilters] = useSetState({
|
||||||
orderBy: 'asc',
|
orderBy: 'asc',
|
||||||
sortBy: 'title',
|
sortBy: AlbumSort.TITLE,
|
||||||
};
|
});
|
||||||
|
|
||||||
const { data: albums } = useAlbums({
|
const { data: albums } = useAlbums({
|
||||||
limit: 0,
|
limit: 0,
|
||||||
page: 0,
|
page: 0,
|
||||||
...params,
|
...filters,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
{albums && (
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
<VirtualInfiniteGrid
|
<Group mb={10} position="apart">
|
||||||
ref={infiniteLoaderRef}
|
<Menu position="bottom-start">
|
||||||
cardControls={{
|
<Menu.Target>
|
||||||
endpoint: albumsApi.getAlbum,
|
<Button variant="subtle">
|
||||||
idProperty: 'id',
|
{
|
||||||
type: Item.Album,
|
FILTERS.find((filter) => filter.value === filters.sortBy)
|
||||||
}}
|
?.name
|
||||||
cardRows={[
|
}
|
||||||
{
|
</Button>
|
||||||
align: 'center',
|
</Menu.Target>
|
||||||
prop: 'name',
|
<Menu.Dropdown>
|
||||||
route: {
|
<Menu.Item
|
||||||
prop: 'id',
|
rightSection={<CaretDown size={12} />}
|
||||||
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
onClick={() => setFilters({ sortBy: AlbumSort.TITLE })}
|
||||||
},
|
>
|
||||||
},
|
Title
|
||||||
{
|
</Menu.Item>
|
||||||
align: 'center',
|
<Menu.Item
|
||||||
prop: 'year',
|
rightSection={<CaretDown size={12} />}
|
||||||
},
|
onClick={() => setFilters({ sortBy: AlbumSort.YEAR })}
|
||||||
]}
|
>
|
||||||
itemCount={albums.pagination.totalEntries}
|
Year
|
||||||
itemGap={20}
|
</Menu.Item>
|
||||||
itemSize={180}
|
<Menu.Item
|
||||||
query={albumsApi.getAlbums}
|
rightSection={<CaretDown size={12} />}
|
||||||
queryParams={params}
|
onClick={() => setFilters({ sortBy: AlbumSort.RATING })}
|
||||||
/>
|
>
|
||||||
)}
|
Rating
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
rightSection={<CaretDown size={12} />}
|
||||||
|
onClick={() => setFilters({ sortBy: AlbumSort.DATE_RELEASED })}
|
||||||
|
>
|
||||||
|
Date Released
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
rightSection={<CaretDown size={12} />}
|
||||||
|
onClick={() => setFilters({ sortBy: AlbumSort.DATE_ADDED })}
|
||||||
|
>
|
||||||
|
Date Added
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item
|
||||||
|
rightSection={<CaretDown size={12} />}
|
||||||
|
onClick={() =>
|
||||||
|
setFilters({ sortBy: AlbumSort.DATE_ADDED_REMOTE })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Date Added (Remote)
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<ViewTypeButton
|
||||||
|
handler={setViewType}
|
||||||
|
menuProps={{ position: 'bottom-end' }}
|
||||||
|
type={viewType}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
<div style={{ flex: 1 }}>
|
||||||
|
{albums && (
|
||||||
|
<AutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<VirtualInfiniteGrid
|
||||||
|
cardControls={{
|
||||||
|
endpoint: albumsApi.getAlbum,
|
||||||
|
idProperty: 'id',
|
||||||
|
type: Item.Album,
|
||||||
|
}}
|
||||||
|
cardRows={[
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
prop: 'name',
|
||||||
|
route: {
|
||||||
|
prop: 'id',
|
||||||
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
align: 'center',
|
||||||
|
prop: 'year',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
height={height}
|
||||||
|
itemCount={albums.pagination.totalEntries}
|
||||||
|
itemGap={20}
|
||||||
|
itemSize={180}
|
||||||
|
minimumBatchSize={100}
|
||||||
|
query={albumsApi.getAlbums}
|
||||||
|
queryParams={filters}
|
||||||
|
width={width}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user