mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Persist album list config
This commit is contained in:
@@ -1,30 +1,11 @@
|
|||||||
import { ax } from '@/renderer/lib/axios';
|
import { ax } from '@/renderer/lib/axios';
|
||||||
import { SortOrder } from '@/renderer/types';
|
import { AlbumSort, SortOrder } from '@/renderer/types';
|
||||||
import {
|
import {
|
||||||
AlbumDetailResponse,
|
AlbumDetailResponse,
|
||||||
AlbumListResponse,
|
AlbumListResponse,
|
||||||
PaginationParams,
|
PaginationParams,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export enum AlbumSort {
|
|
||||||
DATE_ADDED = 'added',
|
|
||||||
DATE_ADDED_REMOTE = 'addedRemote',
|
|
||||||
DATE_RELEASED = 'released',
|
|
||||||
DATE_RELEASED_YEAR = 'year',
|
|
||||||
FAVORITE = 'favorite',
|
|
||||||
NAME = 'name',
|
|
||||||
RANDOM = 'random',
|
|
||||||
RATING = 'rating',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AlbumListParams = PaginationParams & {
|
|
||||||
advancedFilters?: string;
|
|
||||||
orderBy: SortOrder;
|
|
||||||
serverFolderId?: string[];
|
|
||||||
serverUrlId?: string;
|
|
||||||
sortBy: AlbumSort;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAlbumDetail = async (
|
const getAlbumDetail = async (
|
||||||
query: { albumId: string; serverId: string },
|
query: { albumId: string; serverId: string },
|
||||||
signal?: AbortSignal
|
signal?: AbortSignal
|
||||||
@@ -42,6 +23,14 @@ const getAlbumDetail = async (
|
|||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AlbumListParams = PaginationParams & {
|
||||||
|
advancedFilters?: string;
|
||||||
|
orderBy: SortOrder;
|
||||||
|
serverFolderId?: string[];
|
||||||
|
serverUrlId?: string;
|
||||||
|
sortBy: AlbumSort;
|
||||||
|
};
|
||||||
|
|
||||||
const getAlbumList = async (
|
const getAlbumList = async (
|
||||||
query: { serverId: string },
|
query: { serverId: string },
|
||||||
params: AlbumListParams,
|
params: AlbumListParams,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react';
|
import { forwardRef, Ref, useImperativeHandle, useMemo, useState } from 'react';
|
||||||
import { Stack, Group } from '@mantine/core';
|
import { Stack, Group } from '@mantine/core';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
@@ -15,25 +15,11 @@ import {
|
|||||||
TextInput,
|
TextInput,
|
||||||
} from '@/renderer/components';
|
} from '@/renderer/components';
|
||||||
import { useGenreList } from '@/renderer/features/genres';
|
import { useGenreList } from '@/renderer/features/genres';
|
||||||
|
import {
|
||||||
export enum FilterGroupType {
|
AdvancedFilterGroup,
|
||||||
AND = 'AND',
|
AdvancedFilterRule,
|
||||||
OR = 'OR',
|
FilterGroupType,
|
||||||
}
|
} from '@/renderer/types';
|
||||||
|
|
||||||
export type AdvancedFilterRule = {
|
|
||||||
field?: string | null;
|
|
||||||
operator?: string | null;
|
|
||||||
uniqueId: string;
|
|
||||||
value?: string | number | Date | undefined | null | any;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type AdvancedFilterGroup = {
|
|
||||||
group: AdvancedFilterGroup[];
|
|
||||||
rules: AdvancedFilterRule[];
|
|
||||||
type: FilterGroupType;
|
|
||||||
uniqueId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const DATE_FILTER_OPTIONS_DATA = [
|
const DATE_FILTER_OPTIONS_DATA = [
|
||||||
{ label: 'is before', value: '<' },
|
{ label: 'is before', value: '<' },
|
||||||
@@ -787,227 +773,274 @@ const FilterGroup = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdvancedFilters = ({ filters, setFilters }: any) => {
|
const DEFAULT_ADVANCED_FILTERS = {
|
||||||
const handleAddRuleGroup = (args: AddArgs) => {
|
group: [],
|
||||||
const { level, groupIndex } = args;
|
rules: [
|
||||||
const filtersCopy = { ...filters };
|
{
|
||||||
|
field: '',
|
||||||
|
operator: '',
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: FilterGroupType.AND,
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
};
|
||||||
|
|
||||||
const getPath = (level: number) => {
|
interface AdvancedFiltersProps {
|
||||||
if (level === 0) return 'group';
|
defaultFilters?: AdvancedFilterGroup;
|
||||||
|
onChange: (filters: AdvancedFilterGroup) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdvancedFiltersRef {
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdvancedFilters = forwardRef(
|
||||||
|
(
|
||||||
|
{ defaultFilters, onChange }: AdvancedFiltersProps,
|
||||||
|
ref: Ref<AdvancedFiltersRef>
|
||||||
|
) => {
|
||||||
|
const [filters, setFilters] = useState<AdvancedFilterGroup>(
|
||||||
|
defaultFilters || DEFAULT_ADVANCED_FILTERS
|
||||||
|
);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
reset() {
|
||||||
|
setFilters(DEFAULT_ADVANCED_FILTERS);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const setFilterHandler = (newFilters: AdvancedFilterGroup) => {
|
||||||
|
setFilters(newFilters);
|
||||||
|
onChange(newFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddRuleGroup = (args: AddArgs) => {
|
||||||
|
const { level, groupIndex } = args;
|
||||||
|
const filtersCopy = { ...filters };
|
||||||
|
|
||||||
|
const getPath = (level: number) => {
|
||||||
|
if (level === 0) return 'group';
|
||||||
|
|
||||||
|
const str = [];
|
||||||
|
for (const index of groupIndex) {
|
||||||
|
str.push(`group[${index}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str.join('.')}.group`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = getPath(level);
|
||||||
|
const updatedFilters = set(filtersCopy, path, [
|
||||||
|
...get(filtersCopy, path),
|
||||||
|
{
|
||||||
|
group: [],
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
field: '',
|
||||||
|
operator: '',
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: FilterGroupType.AND,
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteRuleGroup = (args: DeleteArgs) => {
|
||||||
|
const { uniqueId, level, groupIndex } = args;
|
||||||
|
const filtersCopy = { ...filters };
|
||||||
|
|
||||||
|
const getPath = (level: number) => {
|
||||||
|
if (level === 0) return 'group';
|
||||||
|
|
||||||
|
const str = [];
|
||||||
|
for (let i = 0; i < groupIndex.length; i += 1) {
|
||||||
|
if (i !== groupIndex.length - 1) {
|
||||||
|
str.push(`group[${groupIndex[i]}]`);
|
||||||
|
} else {
|
||||||
|
str.push(`group`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${str.join('.')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = getPath(level);
|
||||||
|
|
||||||
|
const updatedFilters = set(filtersCopy, path, [
|
||||||
|
...get(filtersCopy, path).filter(
|
||||||
|
(group: AdvancedFilterGroup) => group.uniqueId !== uniqueId
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRulePath = (level: number, groupIndex: number[]) => {
|
||||||
|
if (level === 0) return 'rules';
|
||||||
|
|
||||||
const str = [];
|
const str = [];
|
||||||
for (const index of groupIndex) {
|
for (const index of groupIndex) {
|
||||||
str.push(`group[${index}]`);
|
str.push(`group[${index}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${str.join('.')}.group`;
|
return `${str.join('.')}.rules`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const path = getPath(level);
|
const handleAddRule = (args: AddArgs) => {
|
||||||
const updatedFilters = set(filtersCopy, path, [
|
const { level, groupIndex } = args;
|
||||||
...get(filtersCopy, path),
|
const filtersCopy = { ...filters };
|
||||||
{
|
|
||||||
group: [],
|
const path = getRulePath(level, groupIndex);
|
||||||
rules: [
|
const updatedFilters = set(filtersCopy, path, [
|
||||||
{
|
...get(filtersCopy, path),
|
||||||
field: '',
|
{
|
||||||
operator: '',
|
field: null,
|
||||||
uniqueId: nanoid(),
|
operator: null,
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
value: null,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteRule = (args: DeleteArgs) => {
|
||||||
|
const { uniqueId, level, groupIndex } = args;
|
||||||
|
const filtersCopy = { ...filters };
|
||||||
|
|
||||||
|
const path = getRulePath(level, groupIndex);
|
||||||
|
const updatedFilters = set(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
get(filtersCopy, path).filter(
|
||||||
|
(rule: AdvancedFilterRule) => rule.uniqueId !== uniqueId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
setFilterHandler(updatedFilters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeField = (args: any) => {
|
||||||
|
const { uniqueId, level, groupIndex, value } = args;
|
||||||
|
const filtersCopy = { ...filters };
|
||||||
|
|
||||||
|
const path = getRulePath(level, groupIndex);
|
||||||
|
const updatedFilters = set(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
||||||
|
if (rule.uniqueId !== uniqueId) return rule;
|
||||||
|
const defaultOperator = FILTER_OPTIONS_DATA.find(
|
||||||
|
(option) => option.value === value
|
||||||
|
)?.default;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
field: value,
|
||||||
|
operator: defaultOperator || '',
|
||||||
value: '',
|
value: '',
|
||||||
},
|
};
|
||||||
],
|
})
|
||||||
type: FilterGroupType.AND,
|
);
|
||||||
uniqueId: nanoid(),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
setFilters(updatedFilters);
|
setFilterHandler(updatedFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteRuleGroup = (args: DeleteArgs) => {
|
const handleChangeType = (args: any) => {
|
||||||
const { uniqueId, level, groupIndex } = args;
|
const { level, groupIndex, value } = args;
|
||||||
const filtersCopy = { ...filters };
|
|
||||||
|
|
||||||
const getPath = (level: number) => {
|
const filtersCopy = { ...filters };
|
||||||
if (level === 0) return 'group';
|
|
||||||
|
|
||||||
const str = [];
|
if (level === 0) {
|
||||||
for (let i = 0; i < groupIndex.length; i += 1) {
|
return setFilterHandler({ ...filtersCopy, type: value });
|
||||||
if (i !== groupIndex.length - 1) {
|
}
|
||||||
|
|
||||||
|
const getTypePath = () => {
|
||||||
|
const str = [];
|
||||||
|
for (let i = 0; i < groupIndex.length; i += 1) {
|
||||||
str.push(`group[${groupIndex[i]}]`);
|
str.push(`group[${groupIndex[i]}]`);
|
||||||
} else {
|
|
||||||
str.push(`group`);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return `${str.join('.')}`;
|
return `${str.join('.')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const path = getTypePath();
|
||||||
|
const updatedFilters = set(filtersCopy, path, {
|
||||||
|
...get(filtersCopy, path),
|
||||||
|
type: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
return setFilterHandler(updatedFilters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const path = getPath(level);
|
const handleChangeOperator = (args: any) => {
|
||||||
|
const { uniqueId, level, groupIndex, value } = args;
|
||||||
|
const filtersCopy = { ...filters };
|
||||||
|
|
||||||
const updatedFilters = set(filtersCopy, path, [
|
const path = getRulePath(level, groupIndex);
|
||||||
...get(filtersCopy, path).filter(
|
const updatedFilters = set(
|
||||||
(group: AdvancedFilterGroup) => group.uniqueId !== uniqueId
|
filtersCopy,
|
||||||
),
|
path,
|
||||||
]);
|
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
||||||
|
if (rule.uniqueId !== uniqueId) return rule;
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
operator: value,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
setFilters(updatedFilters);
|
setFilterHandler(updatedFilters);
|
||||||
};
|
|
||||||
|
|
||||||
const getRulePath = (level: number, groupIndex: number[]) => {
|
|
||||||
if (level === 0) return 'rules';
|
|
||||||
|
|
||||||
const str = [];
|
|
||||||
for (const index of groupIndex) {
|
|
||||||
str.push(`group[${index}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${str.join('.')}.rules`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddRule = (args: AddArgs) => {
|
|
||||||
const { level, groupIndex } = args;
|
|
||||||
const filtersCopy = { ...filters };
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
|
||||||
const updatedFilters = set(filtersCopy, path, [
|
|
||||||
...get(filtersCopy, path),
|
|
||||||
{
|
|
||||||
field: null,
|
|
||||||
operator: null,
|
|
||||||
uniqueId: nanoid(),
|
|
||||||
value: null,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
setFilters(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteRule = (args: DeleteArgs) => {
|
|
||||||
const { uniqueId, level, groupIndex } = args;
|
|
||||||
const filtersCopy = { ...filters };
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
|
||||||
const updatedFilters = set(
|
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
get(filtersCopy, path).filter(
|
|
||||||
(rule: AdvancedFilterRule) => rule.uniqueId !== uniqueId
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilters(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeField = (args: any) => {
|
|
||||||
const { uniqueId, level, groupIndex, value } = args;
|
|
||||||
const filtersCopy = { ...filters };
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
|
||||||
const updatedFilters = set(
|
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
|
||||||
if (rule.uniqueId !== uniqueId) return rule;
|
|
||||||
const defaultOperator = FILTER_OPTIONS_DATA.find(
|
|
||||||
(option) => option.value === value
|
|
||||||
)?.default;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...rule,
|
|
||||||
field: value,
|
|
||||||
operator: defaultOperator || '',
|
|
||||||
value: '',
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilters(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeType = (args: any) => {
|
|
||||||
const { level, groupIndex, value } = args;
|
|
||||||
|
|
||||||
const filtersCopy = { ...filters };
|
|
||||||
|
|
||||||
if (level === 0) {
|
|
||||||
return setFilters({ ...filtersCopy, type: value });
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTypePath = () => {
|
|
||||||
const str = [];
|
|
||||||
for (let i = 0; i < groupIndex.length; i += 1) {
|
|
||||||
str.push(`group[${groupIndex[i]}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${str.join('.')}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const path = getTypePath();
|
const handleChangeValue = (args: any) => {
|
||||||
const updatedFilters = set(filtersCopy, path, {
|
const { uniqueId, level, groupIndex, value } = args;
|
||||||
...get(filtersCopy, path),
|
const filtersCopy = { ...filters };
|
||||||
type: value,
|
|
||||||
});
|
|
||||||
|
|
||||||
return setFilters(updatedFilters);
|
const path = getRulePath(level, groupIndex);
|
||||||
};
|
const updatedFilters = set(
|
||||||
|
filtersCopy,
|
||||||
|
path,
|
||||||
|
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
||||||
|
if (rule.uniqueId !== uniqueId) return rule;
|
||||||
|
return {
|
||||||
|
...rule,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const handleChangeOperator = (args: any) => {
|
setFilterHandler(updatedFilters);
|
||||||
const { uniqueId, level, groupIndex, value } = args;
|
};
|
||||||
const filtersCopy = { ...filters };
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
return (
|
||||||
const updatedFilters = set(
|
<>
|
||||||
filtersCopy,
|
<FilterGroup
|
||||||
path,
|
data={filters}
|
||||||
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
groupIndex={[]}
|
||||||
if (rule.uniqueId !== uniqueId) return rule;
|
level={0}
|
||||||
return {
|
uniqueId={filters.uniqueId}
|
||||||
...rule,
|
onAddRule={handleAddRule}
|
||||||
operator: value,
|
onAddRuleGroup={handleAddRuleGroup}
|
||||||
};
|
onChangeField={handleChangeField}
|
||||||
})
|
onChangeOperator={handleChangeOperator}
|
||||||
|
onChangeType={handleChangeType}
|
||||||
|
onChangeValue={handleChangeValue}
|
||||||
|
onDeleteRule={handleDeleteRule}
|
||||||
|
onDeleteRuleGroup={handleDeleteRuleGroup}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
setFilters(updatedFilters);
|
AdvancedFilters.defaultProps = {
|
||||||
};
|
defaultFilters: undefined,
|
||||||
|
|
||||||
const handleChangeValue = (args: any) => {
|
|
||||||
const { uniqueId, level, groupIndex, value } = args;
|
|
||||||
const filtersCopy = { ...filters };
|
|
||||||
|
|
||||||
const path = getRulePath(level, groupIndex);
|
|
||||||
const updatedFilters = set(
|
|
||||||
filtersCopy,
|
|
||||||
path,
|
|
||||||
get(filtersCopy, path).map((rule: AdvancedFilterRule) => {
|
|
||||||
if (rule.uniqueId !== uniqueId) return rule;
|
|
||||||
return {
|
|
||||||
...rule,
|
|
||||||
value,
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setFilters(updatedFilters);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<FilterGroup
|
|
||||||
data={filters}
|
|
||||||
groupIndex={[]}
|
|
||||||
level={0}
|
|
||||||
uniqueId={filters.uniqueId}
|
|
||||||
onAddRule={handleAddRule}
|
|
||||||
onAddRuleGroup={handleAddRuleGroup}
|
|
||||||
onChangeField={handleChangeField}
|
|
||||||
onChangeOperator={handleChangeOperator}
|
|
||||||
onChangeType={handleChangeType}
|
|
||||||
onChangeValue={handleChangeValue}
|
|
||||||
onDeleteRule={handleDeleteRule}
|
|
||||||
onDeleteRuleGroup={handleDeleteRuleGroup}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable no-plusplus */
|
/* eslint-disable no-plusplus */
|
||||||
import { useState, useCallback, useMemo, MouseEvent } from 'react';
|
import { useCallback, useMemo, MouseEvent, useRef } from 'react';
|
||||||
import { Group, Box } from '@mantine/core';
|
import { Group, Box } from '@mantine/core';
|
||||||
import { useDebouncedValue, useSetState, useToggle } from '@mantine/hooks';
|
import { useDebouncedValue } from '@mantine/hooks';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
|
import debounce from 'lodash/debounce';
|
||||||
import throttle from 'lodash/throttle';
|
import throttle from 'lodash/throttle';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import {
|
import {
|
||||||
@@ -12,8 +13,8 @@ import {
|
|||||||
RiSettings2Fill,
|
RiSettings2Fill,
|
||||||
} from 'react-icons/ri';
|
} from 'react-icons/ri';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
|
import { ListOnScrollProps } from 'react-window';
|
||||||
import { api } from '@/renderer/api';
|
import { api } from '@/renderer/api';
|
||||||
import { AlbumSort } from '@/renderer/api/albums.api';
|
|
||||||
import { queryKeys } from '@/renderer/api/query-keys';
|
import { queryKeys } from '@/renderer/api/query-keys';
|
||||||
import { SortOrder } from '@/renderer/api/types';
|
import { SortOrder } from '@/renderer/api/types';
|
||||||
import {
|
import {
|
||||||
@@ -29,17 +30,22 @@ import {
|
|||||||
VirtualInfiniteGrid,
|
VirtualInfiniteGrid,
|
||||||
} from '@/renderer/components';
|
} from '@/renderer/components';
|
||||||
import {
|
import {
|
||||||
AdvancedFilterGroup,
|
|
||||||
AdvancedFilters,
|
AdvancedFilters,
|
||||||
FilterGroupType,
|
|
||||||
encodeAdvancedFiltersQuery,
|
encodeAdvancedFiltersQuery,
|
||||||
|
AdvancedFiltersRef,
|
||||||
} from '@/renderer/features/albums/components/advanced-filters';
|
} from '@/renderer/features/albums/components/advanced-filters';
|
||||||
import { useAlbumList } from '@/renderer/features/albums/queries/get-album-list';
|
import { useAlbumList } from '@/renderer/features/albums/queries/get-album-list';
|
||||||
import { useServerList } from '@/renderer/features/servers';
|
import { useServerList } from '@/renderer/features/servers';
|
||||||
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
|
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
|
||||||
import { AppRoute } from '@/renderer/router/routes';
|
import { AppRoute } from '@/renderer/router/routes';
|
||||||
import { useAppStore, useAuthStore } from '@/renderer/store';
|
import { useAppStore, useAuthStore } from '@/renderer/store';
|
||||||
import { LibraryItem, CardDisplayType } from '@/renderer/types';
|
import {
|
||||||
|
LibraryItem,
|
||||||
|
CardDisplayType,
|
||||||
|
AlbumSort,
|
||||||
|
FilterGroupType,
|
||||||
|
AdvancedFilterGroup,
|
||||||
|
} from '@/renderer/types';
|
||||||
|
|
||||||
const FILTERS = [
|
const FILTERS = [
|
||||||
{ name: 'Title', value: AlbumSort.NAME },
|
{ name: 'Title', value: AlbumSort.NAME },
|
||||||
@@ -81,19 +87,16 @@ export const AlbumListRoute = () => {
|
|||||||
const setPage = useAppStore((state) => state.setPage);
|
const setPage = useAppStore((state) => state.setPage);
|
||||||
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
|
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
|
||||||
const serverListQuery = useServerList({ enabled: true });
|
const serverListQuery = useServerList({ enabled: true });
|
||||||
const [filters, setFilters] = useSetState({
|
const filters = page.list.filter;
|
||||||
orderBy: SortOrder.ASC,
|
const advancedFiltersRef = useRef<AdvancedFiltersRef>(null);
|
||||||
serverFolderId: [] as string[],
|
|
||||||
sortBy: AlbumSort.NAME,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isAdvFilter, toggleAdvFilter] = useToggle();
|
const isAdvFilter = page.list.advancedFilter.enabled;
|
||||||
const [rawAdvFilters, setRawAdvFilters] = useState<AdvancedFilterGroup>(
|
|
||||||
DEFAULT_ADVANCED_FILTERS
|
const [debouncedAdvFilters] = useDebouncedValue(
|
||||||
|
page.list.advancedFilter.filter,
|
||||||
|
500
|
||||||
);
|
);
|
||||||
|
|
||||||
const [debouncedAdvFilters] = useDebouncedValue(rawAdvFilters, 500);
|
|
||||||
|
|
||||||
const advancedFilters = useMemo(() => {
|
const advancedFilters = useMemo(() => {
|
||||||
if (!isAdvFilter) {
|
if (!isAdvFilter) {
|
||||||
return encodeAdvancedFiltersQuery(DEFAULT_ADVANCED_FILTERS);
|
return encodeAdvancedFiltersQuery(DEFAULT_ADVANCED_FILTERS);
|
||||||
@@ -103,7 +106,17 @@ export const AlbumListRoute = () => {
|
|||||||
}, [debouncedAdvFilters, isAdvFilter]);
|
}, [debouncedAdvFilters, isAdvFilter]);
|
||||||
|
|
||||||
const handleResetAdvancedFilters = () => {
|
const handleResetAdvancedFilters = () => {
|
||||||
setRawAdvFilters(DEFAULT_ADVANCED_FILTERS);
|
setPage('albums', {
|
||||||
|
...page,
|
||||||
|
list: {
|
||||||
|
...page.list,
|
||||||
|
advancedFilter: {
|
||||||
|
...page.list.advancedFilter,
|
||||||
|
filter: DEFAULT_ADVANCED_FILTERS,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
advancedFiltersRef.current?.reset();
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverFolders = useMemo(() => {
|
const serverFolders = useMemo(() => {
|
||||||
@@ -173,32 +186,84 @@ export const AlbumListRoute = () => {
|
|||||||
|
|
||||||
const handleSetFilter = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleSetFilter = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!e.currentTarget?.value) return;
|
if (!e.currentTarget?.value) return;
|
||||||
setFilters({ sortBy: e.currentTarget.value as AlbumSort });
|
setPage('albums', {
|
||||||
|
list: {
|
||||||
|
...page.list,
|
||||||
|
filter: {
|
||||||
|
...page.list.filter,
|
||||||
|
sortBy: e.currentTarget.value as AlbumSort,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetOrder = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleSetOrder = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!e.currentTarget?.value) return;
|
if (!e.currentTarget?.value) return;
|
||||||
setFilters({ orderBy: e.currentTarget.value as SortOrder });
|
setPage('albums', {
|
||||||
|
list: {
|
||||||
|
...page.list,
|
||||||
|
filter: {
|
||||||
|
...page.list.filter,
|
||||||
|
orderBy: e.currentTarget.value as SortOrder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSetServerFolder = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleSetServerFolder = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!e.currentTarget?.value) return;
|
if (!e.currentTarget?.value) return;
|
||||||
const value = e.currentTarget.value as string;
|
const value = e.currentTarget.value as string;
|
||||||
if (filters.serverFolderId.includes(value)) {
|
if (filters.serverFolderId.includes(value)) {
|
||||||
setFilters({
|
setPage('albums', {
|
||||||
serverFolderId: filters.serverFolderId.filter((id) => id !== value),
|
list: {
|
||||||
|
...page.list,
|
||||||
|
filter: {
|
||||||
|
...page.list.filter,
|
||||||
|
serverFolderId: filters.serverFolderId.filter((id) => id !== value),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setFilters({
|
setPage('albums', {
|
||||||
serverFolderId: [...filters.serverFolderId, value],
|
list: {
|
||||||
|
...page.list,
|
||||||
|
filter: {
|
||||||
|
...page.list.filter,
|
||||||
|
serverFolderId: [...filters.serverFolderId, value],
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleAdvancedFilters = () => {
|
const handleToggleAdvancedFilters = () => {
|
||||||
toggleAdvFilter();
|
const enabled = !page.list.advancedFilter.enabled;
|
||||||
|
setPage('albums', {
|
||||||
|
...page,
|
||||||
|
list: {
|
||||||
|
...page.list,
|
||||||
|
advancedFilter: {
|
||||||
|
...page.list.advancedFilter,
|
||||||
|
enabled,
|
||||||
|
...(!enabled && { filter: DEFAULT_ADVANCED_FILTERS }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateAdvancedFilters = debounce((e: AdvancedFilterGroup) => {
|
||||||
|
setPage('albums', {
|
||||||
|
...page,
|
||||||
|
list: {
|
||||||
|
...page.list,
|
||||||
|
advancedFilter: {
|
||||||
|
...page.list.advancedFilter,
|
||||||
|
filter: e,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 150);
|
||||||
|
|
||||||
const handleSetViewType = (e: MouseEvent<HTMLButtonElement>) => {
|
const handleSetViewType = (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
if (!e.currentTarget?.value) return;
|
if (!e.currentTarget?.value) return;
|
||||||
const type = e.currentTarget.value;
|
const type = e.currentTarget.value;
|
||||||
@@ -231,6 +296,16 @@ export const AlbumListRoute = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGridScroll = debounce((e: ListOnScrollProps) => {
|
||||||
|
setPage('albums', {
|
||||||
|
...page,
|
||||||
|
list: {
|
||||||
|
...page.list,
|
||||||
|
gridScrollOffset: e.scrollOffset,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<VirtualGridContainer>
|
<VirtualGridContainer>
|
||||||
@@ -418,8 +493,9 @@ export const AlbumListRoute = () => {
|
|||||||
</Group>
|
</Group>
|
||||||
<Box p={10}>
|
<Box p={10}>
|
||||||
<AdvancedFilters
|
<AdvancedFilters
|
||||||
filters={rawAdvFilters}
|
ref={advancedFiltersRef}
|
||||||
setFilters={setRawAdvFilters}
|
defaultFilters={page.list.advancedFilter.filter}
|
||||||
|
onChange={handleUpdateAdvancedFilters}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -456,6 +532,7 @@ export const AlbumListRoute = () => {
|
|||||||
display={page.list?.display || CardDisplayType.CARD}
|
display={page.list?.display || CardDisplayType.CARD}
|
||||||
fetchFn={fetch}
|
fetchFn={fetch}
|
||||||
height={height}
|
height={height}
|
||||||
|
initialScrollOffset={page.list?.gridScrollOffset || 0}
|
||||||
itemCount={albumListQuery?.data?.pagination.totalEntries || 0}
|
itemCount={albumListQuery?.data?.pagination.totalEntries || 0}
|
||||||
itemGap={20}
|
itemGap={20}
|
||||||
itemSize={150 + page.list?.size}
|
itemSize={150 + page.list?.size}
|
||||||
@@ -467,6 +544,7 @@ export const AlbumListRoute = () => {
|
|||||||
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
slugs: [{ idProperty: 'id', slugProperty: 'albumId' }],
|
||||||
}}
|
}}
|
||||||
width={width}
|
width={width}
|
||||||
|
onScroll={handleGridScroll}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
import create from 'zustand';
|
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 { CardDisplayType, Platform } from '@/renderer/types';
|
import {
|
||||||
|
AdvancedFilterGroup,
|
||||||
|
AlbumSort,
|
||||||
|
CardDisplayType,
|
||||||
|
FilterGroupType,
|
||||||
|
Platform,
|
||||||
|
SortOrder,
|
||||||
|
} from '@/renderer/types';
|
||||||
|
|
||||||
type SidebarProps = {
|
type SidebarProps = {
|
||||||
expanded: string[];
|
expanded: string[];
|
||||||
@@ -16,8 +24,24 @@ type LibraryPageProps = {
|
|||||||
list: ListProps;
|
list: ListProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type ListFilter = {
|
||||||
|
orderBy: SortOrder;
|
||||||
|
search?: string;
|
||||||
|
serverFolderId: string[];
|
||||||
|
sortBy: AlbumSort;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ListAdvancedFilter = {
|
||||||
|
enabled: boolean;
|
||||||
|
filter: AdvancedFilterGroup;
|
||||||
|
};
|
||||||
|
|
||||||
type ListProps = {
|
type ListProps = {
|
||||||
|
advancedFilter: ListAdvancedFilter;
|
||||||
display: CardDisplayType;
|
display: CardDisplayType;
|
||||||
|
filter: ListFilter;
|
||||||
|
gridScrollOffset: number;
|
||||||
|
listScrollOffset: number;
|
||||||
size: number;
|
size: number;
|
||||||
type: 'list' | 'grid';
|
type: 'list' | 'grid';
|
||||||
};
|
};
|
||||||
@@ -35,6 +59,20 @@ export interface AppState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ADVANCED_FILTERS = {
|
||||||
|
group: [],
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
field: '',
|
||||||
|
operator: '',
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: FilterGroupType.AND,
|
||||||
|
uniqueId: nanoid(),
|
||||||
|
};
|
||||||
|
|
||||||
export interface AppSlice extends AppState {
|
export interface AppSlice extends AppState {
|
||||||
setAppStore: (data: Partial<AppSlice>) => void;
|
setAppStore: (data: Partial<AppSlice>) => void;
|
||||||
setPage: (page: 'albums', options: Partial<LibraryPageProps>) => void;
|
setPage: (page: 'albums', options: Partial<LibraryPageProps>) => void;
|
||||||
@@ -47,7 +85,19 @@ export const useAppStore = create<AppSlice>()(
|
|||||||
immer((set, get) => ({
|
immer((set, get) => ({
|
||||||
albums: {
|
albums: {
|
||||||
list: {
|
list: {
|
||||||
|
advancedFilter: {
|
||||||
|
enabled: false,
|
||||||
|
filter: DEFAULT_ADVANCED_FILTERS,
|
||||||
|
},
|
||||||
display: CardDisplayType.CARD,
|
display: CardDisplayType.CARD,
|
||||||
|
filter: {
|
||||||
|
orderBy: SortOrder.DESC,
|
||||||
|
search: '',
|
||||||
|
serverFolderId: [],
|
||||||
|
sortBy: AlbumSort.DATE_ADDED_REMOTE,
|
||||||
|
},
|
||||||
|
gridScrollOffset: 0,
|
||||||
|
listScrollOffset: 0,
|
||||||
size: 50,
|
size: 50,
|
||||||
type: 'grid',
|
type: 'grid',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -91,11 +91,41 @@ export interface UniqueId {
|
|||||||
uniqueId: string;
|
uniqueId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AlbumSort {
|
||||||
|
DATE_ADDED = 'added',
|
||||||
|
DATE_ADDED_REMOTE = 'addedRemote',
|
||||||
|
DATE_RELEASED = 'released',
|
||||||
|
DATE_RELEASED_YEAR = 'year',
|
||||||
|
FAVORITE = 'favorite',
|
||||||
|
NAME = 'name',
|
||||||
|
RANDOM = 'random',
|
||||||
|
RATING = 'rating',
|
||||||
|
}
|
||||||
|
|
||||||
export enum SortOrder {
|
export enum SortOrder {
|
||||||
ASC = 'asc',
|
ASC = 'asc',
|
||||||
DESC = 'desc',
|
DESC = 'desc',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum FilterGroupType {
|
||||||
|
AND = 'AND',
|
||||||
|
OR = 'OR',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdvancedFilterRule = {
|
||||||
|
field?: string | null;
|
||||||
|
operator?: string | null;
|
||||||
|
uniqueId: string;
|
||||||
|
value?: string | number | Date | undefined | null | any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AdvancedFilterGroup = {
|
||||||
|
group: AdvancedFilterGroup[];
|
||||||
|
rules: AdvancedFilterRule[];
|
||||||
|
type: FilterGroupType;
|
||||||
|
uniqueId: string;
|
||||||
|
};
|
||||||
|
|
||||||
export enum TableColumn {
|
export enum TableColumn {
|
||||||
ALBUM = 'album',
|
ALBUM = 'album',
|
||||||
ALBUM_ARTIST = 'albumArtist',
|
ALBUM_ARTIST = 'albumArtist',
|
||||||
|
|||||||
Reference in New Issue
Block a user