mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-24 21:07:41 +02:00
450 lines
14 KiB
TypeScript
450 lines
14 KiB
TypeScript
/* eslint-disable no-plusplus */
|
|
import { useState, useCallback, useMemo } from 'react';
|
|
import { Group, Box, Slider } from '@mantine/core';
|
|
import { useDebouncedValue, useSetState, useToggle } from '@mantine/hooks';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import throttle from 'lodash/throttle';
|
|
import { nanoid } from 'nanoid';
|
|
import {
|
|
RiArrowDownSLine,
|
|
RiDeleteBack2Fill,
|
|
RiSettings2Fill,
|
|
} from 'react-icons/ri';
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
import { api } from '@/renderer/api';
|
|
import { AlbumSort } from '@/renderer/api/albums.api';
|
|
import { queryKeys } from '@/renderer/api/query-keys';
|
|
import { SortOrder } from '@/renderer/api/types';
|
|
import {
|
|
Button,
|
|
DropdownMenu,
|
|
NumberInput,
|
|
ScrollArea,
|
|
Paper,
|
|
Text,
|
|
VirtualGridAutoSizerContainer,
|
|
VirtualGridContainer,
|
|
VirtualInfiniteGrid,
|
|
} from '@/renderer/components';
|
|
import {
|
|
AdvancedFilterGroup,
|
|
AdvancedFilters,
|
|
FilterGroupType,
|
|
encodeAdvancedFiltersQuery,
|
|
} from '@/renderer/features/albums/components/advanced-filters';
|
|
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list';
|
|
import { useServerList } from '@/renderer/features/servers';
|
|
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
|
|
import { AppRoute } from '@/renderer/router/routes';
|
|
import { useAppStore, useAuthStore } from '@/renderer/store';
|
|
import { LibraryItem, CardDisplayType } from '@/renderer/types';
|
|
|
|
const FILTERS = [
|
|
{ name: 'Title', value: AlbumSort.NAME },
|
|
{ name: 'Date added', value: AlbumSort.DATE_ADDED },
|
|
{
|
|
name: 'Date Added (remote)',
|
|
value: AlbumSort.DATE_ADDED_REMOTE,
|
|
},
|
|
{ name: 'Release Date', value: AlbumSort.DATE_RELEASED },
|
|
{ name: 'Year', value: AlbumSort.DATE_RELEASED_YEAR },
|
|
{ name: 'Random', value: AlbumSort.RANDOM },
|
|
{ name: 'Favorites', value: AlbumSort.FAVORITE },
|
|
{ name: 'Rating', value: AlbumSort.RATING },
|
|
];
|
|
|
|
const ORDER = [
|
|
{ name: 'Ascending', value: SortOrder.ASC },
|
|
{ name: 'Descending', value: SortOrder.DESC },
|
|
];
|
|
|
|
const DEFAULT_ADVANCED_FILTERS = {
|
|
group: [],
|
|
rules: [
|
|
{
|
|
field: '',
|
|
operator: '',
|
|
uniqueId: nanoid(),
|
|
value: '',
|
|
},
|
|
],
|
|
type: FilterGroupType.AND,
|
|
uniqueId: nanoid(),
|
|
};
|
|
|
|
export const AlbumListRoute = () => {
|
|
const queryClient = useQueryClient();
|
|
const { serverToken, isImageTokenRequired } = useServerCredential();
|
|
const page = useAppStore((state) => state.albums);
|
|
const setPage = useAppStore((state) => state.setPage);
|
|
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
|
|
const { data: servers } = useServerList({ enabled: true });
|
|
const [filters, setFilters] = useSetState({
|
|
orderBy: SortOrder.ASC,
|
|
serverFolderId: [] as string[],
|
|
sortBy: AlbumSort.NAME,
|
|
});
|
|
|
|
const [isAdvFilter, toggleAdvFilter] = useToggle();
|
|
const [rawAdvFilters, setRawAdvFilters] = useState<AdvancedFilterGroup>(
|
|
DEFAULT_ADVANCED_FILTERS
|
|
);
|
|
|
|
const [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 500);
|
|
|
|
const advancedFilters = useMemo(() => {
|
|
if (!isAdvFilter) {
|
|
return encodeAdvancedFiltersQuery(DEFAULT_ADVANCED_FILTERS);
|
|
}
|
|
|
|
return encodeAdvancedFiltersQuery(debouncedAdvFilters);
|
|
}, [debouncedAdvFilters, isAdvFilter]);
|
|
|
|
const handleResetAdvancedFilters = () => {
|
|
setRawAdvFilters(DEFAULT_ADVANCED_FILTERS);
|
|
};
|
|
|
|
const serverFolders = useMemo(() => {
|
|
const server = servers?.data.find((server) => server.id === serverId);
|
|
return server?.serverFolders;
|
|
}, [serverId, servers]);
|
|
|
|
const { data: albums } = useAlbumList({
|
|
advancedFilters,
|
|
orderBy: filters.orderBy,
|
|
serverFolderId: filters.serverFolderId,
|
|
skip: 0,
|
|
sortBy: filters.sortBy,
|
|
take: 0,
|
|
});
|
|
|
|
const fetch = useCallback(
|
|
async ({ skip, take }: { skip: number; take: number }) => {
|
|
const albums = await queryClient.fetchQuery(
|
|
queryKeys.albums.list(serverId, {
|
|
skip,
|
|
take,
|
|
...filters,
|
|
advancedFilters,
|
|
}),
|
|
async () =>
|
|
api.albums.getAlbumList(
|
|
{ serverId },
|
|
{ skip, take, ...filters, advancedFilters }
|
|
)
|
|
);
|
|
|
|
// * Adds server token
|
|
if (isImageTokenRequired) {
|
|
const data = albums.data.map((album) => {
|
|
return {
|
|
...album,
|
|
imageUrl: album.imageUrl && album.imageUrl + serverToken!,
|
|
};
|
|
});
|
|
|
|
return { ...albums, data };
|
|
}
|
|
|
|
return albums;
|
|
},
|
|
[
|
|
advancedFilters,
|
|
filters,
|
|
isImageTokenRequired,
|
|
queryClient,
|
|
serverId,
|
|
serverToken,
|
|
]
|
|
);
|
|
|
|
const setSize = throttle(
|
|
(e: number) =>
|
|
setPage('albums', {
|
|
...page,
|
|
list: { ...page.list, size: e },
|
|
}),
|
|
200
|
|
);
|
|
|
|
return (
|
|
<AnimatedPage>
|
|
<VirtualGridContainer>
|
|
<Group m={10} position="apart">
|
|
<Group>
|
|
<Text noSelect size="lg">
|
|
Albums
|
|
</Text>
|
|
<DropdownMenu position="bottom-start">
|
|
<DropdownMenu.Target>
|
|
<Button compact variant="subtle">
|
|
<Group>
|
|
{FILTERS.find((f) => f.value === filters.sortBy)?.name}{' '}
|
|
<RiArrowDownSLine size={15} />
|
|
</Group>
|
|
</Button>
|
|
</DropdownMenu.Target>
|
|
<DropdownMenu.Dropdown>
|
|
{FILTERS.map((filter) => (
|
|
<DropdownMenu.Item
|
|
key={`filter-${filter.value}`}
|
|
isActive={filter.value === filters.sortBy}
|
|
onClick={() => setFilters({ sortBy: filter.value })}
|
|
>
|
|
{filter.name}
|
|
</DropdownMenu.Item>
|
|
))}
|
|
<DropdownMenu.Divider />
|
|
<DropdownMenu.Item
|
|
isActive={isAdvFilter}
|
|
onClick={() => toggleAdvFilter()}
|
|
>
|
|
Advanced Filters
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Dropdown>
|
|
</DropdownMenu>
|
|
<DropdownMenu position="bottom-start">
|
|
<DropdownMenu.Target>
|
|
<Button compact variant="subtle">
|
|
<Group>
|
|
{ORDER.find((s) => s.value === filters.orderBy)?.name}{' '}
|
|
<RiArrowDownSLine size={15} />
|
|
</Group>
|
|
</Button>
|
|
</DropdownMenu.Target>
|
|
<DropdownMenu.Dropdown>
|
|
{ORDER.map((sort) => (
|
|
<DropdownMenu.Item
|
|
key={`sort-${sort.value}`}
|
|
isActive={sort.value === filters.orderBy}
|
|
onClick={() => setFilters({ orderBy: sort.value })}
|
|
>
|
|
{sort.name}
|
|
</DropdownMenu.Item>
|
|
))}
|
|
</DropdownMenu.Dropdown>
|
|
</DropdownMenu>
|
|
<DropdownMenu position="bottom-start">
|
|
<DropdownMenu.Target>
|
|
<Button compact variant="subtle">
|
|
<Group>
|
|
Folders <RiArrowDownSLine size={15} />
|
|
</Group>
|
|
</Button>
|
|
</DropdownMenu.Target>
|
|
<DropdownMenu.Dropdown>
|
|
{serverFolders?.map((folder) => (
|
|
<DropdownMenu.Item
|
|
key={folder.id}
|
|
closeMenuOnClick={false}
|
|
isActive={filters.serverFolderId.includes(folder.id)}
|
|
onClick={() => {
|
|
if (filters.serverFolderId.includes(folder.id)) {
|
|
setFilters({
|
|
serverFolderId: filters.serverFolderId.filter(
|
|
(id) => id !== folder.id
|
|
),
|
|
});
|
|
} else {
|
|
setFilters({
|
|
serverFolderId: [
|
|
...filters.serverFolderId,
|
|
folder.id,
|
|
],
|
|
});
|
|
}
|
|
}}
|
|
>
|
|
{folder.name}
|
|
</DropdownMenu.Item>
|
|
))}
|
|
</DropdownMenu.Dropdown>
|
|
</DropdownMenu>
|
|
</Group>
|
|
<Group position="right">
|
|
<DropdownMenu position="bottom-end" width={100}>
|
|
<DropdownMenu.Target>
|
|
<Button compact variant="subtle">
|
|
<RiSettings2Fill size={15} />
|
|
</Button>
|
|
</DropdownMenu.Target>
|
|
<DropdownMenu.Dropdown>
|
|
<DropdownMenu.Item>
|
|
<Slider
|
|
defaultValue={page.list?.size || 0}
|
|
label={null}
|
|
onChange={setSize}
|
|
/>
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Divider />
|
|
<DropdownMenu.Item
|
|
isActive={
|
|
page.list.type === 'grid' &&
|
|
page.list.display === CardDisplayType.CARD
|
|
}
|
|
onClick={() =>
|
|
setPage('albums', {
|
|
...page,
|
|
list: {
|
|
...page.list,
|
|
display: CardDisplayType.CARD,
|
|
type: 'grid',
|
|
},
|
|
})
|
|
}
|
|
>
|
|
Card
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Item
|
|
isActive={
|
|
page.list.type === 'grid' &&
|
|
page.list.display === CardDisplayType.POSTER
|
|
}
|
|
onClick={() =>
|
|
setPage('albums', {
|
|
...page,
|
|
list: {
|
|
...page.list,
|
|
display: CardDisplayType.POSTER,
|
|
type: 'grid',
|
|
},
|
|
})
|
|
}
|
|
>
|
|
Poster
|
|
</DropdownMenu.Item>
|
|
<DropdownMenu.Item
|
|
disabled
|
|
isActive={page.list.type === 'list'}
|
|
onClick={() =>
|
|
setPage('albums', {
|
|
...page,
|
|
list: {
|
|
...page.list,
|
|
type: 'list',
|
|
},
|
|
})
|
|
}
|
|
>
|
|
List
|
|
</DropdownMenu.Item>
|
|
</DropdownMenu.Dropdown>
|
|
</DropdownMenu>
|
|
</Group>
|
|
</Group>
|
|
<AnimatePresence
|
|
key="album-list-advanced-filter"
|
|
exitBeforeEnter
|
|
initial={false}
|
|
>
|
|
{isAdvFilter && (
|
|
<motion.div
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -25 }}
|
|
initial={{ opacity: 0, y: -25 }}
|
|
style={{ maxHeight: '20vh', zIndex: 100 }}
|
|
transition={{ duration: 0.2, ease: 'easeInOut' }}
|
|
>
|
|
<Paper
|
|
sx={{
|
|
boxShadow: ' 0 10px 5px -2px rgb(0, 0, 0, .2)',
|
|
height: '100%',
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
<ScrollArea sx={{ height: '100%', width: '100%' }}>
|
|
<Group
|
|
noWrap
|
|
p={10}
|
|
position="apart"
|
|
sx={{
|
|
background: 'var(--paper-bg)',
|
|
position: 'sticky',
|
|
top: 0,
|
|
zIndex: 50,
|
|
}}
|
|
>
|
|
<Group>
|
|
<Text noSelect>Advanced Filters</Text>
|
|
<NumberInput
|
|
disabled
|
|
min={1}
|
|
placeholder="Limit"
|
|
size="xs"
|
|
width={75}
|
|
/>
|
|
<Button
|
|
px={10}
|
|
size="xs"
|
|
tooltip={{ label: 'Reset' }}
|
|
variant="default"
|
|
onClick={handleResetAdvancedFilters}
|
|
>
|
|
<RiDeleteBack2Fill size={15} />
|
|
</Button>
|
|
</Group>
|
|
<Button disabled uppercase variant="default">
|
|
Save as...
|
|
</Button>
|
|
</Group>
|
|
<Box p={10}>
|
|
<AdvancedFilters
|
|
filters={rawAdvFilters}
|
|
setFilters={setRawAdvFilters}
|
|
/>
|
|
</Box>
|
|
</ScrollArea>
|
|
</Paper>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
<VirtualGridAutoSizerContainer>
|
|
<AutoSizer>
|
|
{({ height, width }) => (
|
|
<VirtualInfiniteGrid
|
|
cardRows={[
|
|
{
|
|
property: 'name',
|
|
route: {
|
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
|
},
|
|
},
|
|
{
|
|
arrayProperty: 'name',
|
|
property: 'albumArtists',
|
|
route: {
|
|
route: AppRoute.LIBRARY_ALBUMARTISTS_DETAIL,
|
|
slugs: [
|
|
{ idProperty: 'id', slugProperty: 'albumArtistId' },
|
|
],
|
|
},
|
|
},
|
|
{
|
|
property: 'releaseYear',
|
|
},
|
|
]}
|
|
display={page.list?.display || CardDisplayType.CARD}
|
|
fetchFn={fetch}
|
|
height={height}
|
|
itemCount={albums?.pagination.totalEntries || 0}
|
|
itemGap={20}
|
|
itemSize={150 + page.list?.size}
|
|
itemType={LibraryItem.ALBUM}
|
|
minimumBatchSize={40}
|
|
refresh={advancedFilters}
|
|
route={{
|
|
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
|
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
|
}}
|
|
width={width}
|
|
/>
|
|
)}
|
|
</AutoSizer>
|
|
</VirtualGridAutoSizerContainer>
|
|
</VirtualGridContainer>
|
|
</AnimatedPage>
|
|
);
|
|
};
|