Skip to content

Commit

Permalink
Add search to collection
Browse files Browse the repository at this point in the history
  • Loading branch information
harshithmohan committed Dec 4, 2023
1 parent 494cc87 commit 5118d3c
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 17 deletions.
9 changes: 8 additions & 1 deletion src/components/Collection/CollectionTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ import cx from 'classnames';
type Props = {
count: number;
filterOrGroup?: string;
searchQuery: string;
};

const CollectionTitle = ({ count, filterOrGroup }: Props) => (
const CollectionTitle = ({ count, filterOrGroup, searchQuery }: Props) => (
<div className="flex items-center gap-x-2 text-xl font-semibold">
<Link to="/webui/collection" className={cx(filterOrGroup ? 'text-panel-text-primary' : 'pointer-events-none')}>
Entire Collection
Expand All @@ -20,6 +21,12 @@ const CollectionTitle = ({ count, filterOrGroup }: Props) => (
{filterOrGroup}
</>
)}
{searchQuery && (
<>
<Icon path={mdiChevronRight} size={1} />
Search Results
</>
)}
<span>|</span>
<span className="text-panel-text-important">
{/* Count is set to -1 when series data is empty and is used as a flag to signify that in other places */}
Expand Down
67 changes: 59 additions & 8 deletions src/components/Collection/CollectionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,23 @@ import { debounce } from 'lodash';

import ListViewItem from '@/components/Collection/ListViewItem';
import PosterViewItem from '@/components/Collection/PosterViewItem';
import { useLazyGetGroupSeriesQuery, useLazyGetGroupsQuery } from '@/core/rtkQuery/splitV3Api/collectionApi';
import buildFilter from '@/core/buildFilter';
import { useLazyGetGroupSeriesQuery } from '@/core/rtkQuery/splitV3Api/collectionApi';
import { useGetFilterQuery, useLazyGetFilteredGroupsQuery } from '@/core/rtkQuery/splitV3Api/filterApi';
import { useGetSettingsQuery } from '@/core/rtkQuery/splitV3Api/settingsApi';
import { useLazyGetGroupViewQuery } from '@/core/rtkQuery/splitV3Api/webuiApi';
import { initialSettings } from '@/pages/settings/SettingsPage';

import type { InfiniteResultType } from '@/core/types/api';
import type { FilterCondition, FilterType } from '@/core/types/api/filter';
import type { SeriesType } from '@/core/types/api/series';

type Props = {
mode: string;
setGroupTotal: (total: number) => void;
setTimelineSeries: (series: SeriesType[]) => void;
isSidebarOpen: boolean;
searchQuery: string;
};

const defaultPageSize = 50;
Expand All @@ -40,10 +44,40 @@ export const listItemSize = {
gap: 32,
};

const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries }: Props) => {
const getFilter = (query: string, filterCondition?: FilterCondition): FilterType => {
let finalCondition: FilterCondition | undefined;
if (query) {
const searchCondition: FilterCondition = {
Type: 'StringFuzzyMatches',
Left: {
Type: 'NameSelector',
},
Parameter: query,
};

if (filterCondition) {
finalCondition = buildFilter([searchCondition, filterCondition]);
} else {
finalCondition = buildFilter([searchCondition]);
}
} else if (filterCondition) {
finalCondition = buildFilter([filterCondition]);
}

return (
finalCondition
? {
Expression: finalCondition,
}
: {}
);
};

const CollectionView = ({ isSidebarOpen, mode, searchQuery, setGroupTotal, setTimelineSeries }: Props) => {
const { filterId, groupId } = useParams();

const [currentFilterId, setCurrentFilterId] = useState(filterId);
const [currentSearch, setCurrentSearch] = useState(searchQuery);

const settingsQuery = useGetSettingsQuery();
const settings = useMemo(() => settingsQuery?.data ?? initialSettings, [settingsQuery]);
Expand All @@ -54,6 +88,8 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
[mode, showRandomPosterGrid, showRandomPosterList],
);

const filterQuery = useGetFilterQuery({ filterId: filterId! }, { skip: !filterId });

const [itemWidth, itemHeight, itemGap] = useMemo(() => {
if (mode === 'poster') return [posterItemSize.width, posterItemSize.height, posterItemSize.gap];
return [
Expand All @@ -65,7 +101,7 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries

const [fetchingPage, setFetchingPage] = useState(false);

const [fetchGroups, groupsData] = useLazyGetGroupsQuery();
const [fetchGroups, groupsData] = useLazyGetFilteredGroupsQuery();
const [fetchSeries, seriesDataResult] = useLazyGetGroupSeriesQuery();
const [seriesData, setSeriesData] = useState<InfiniteResultType<SeriesType[]>>({ pages: [], total: -1 });
// This is to set an extra arg for groupsQuery so that cache is invalidated correctly. Using state because this should not change once component is mounted.
Expand Down Expand Up @@ -100,11 +136,13 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
fetchGroups({
page,
pageSize,
filterId: filterId ?? '0',
randomImages: showRandomPoster,
filterCriteria: getFilter(searchQuery, filterId ? filterQuery.data?.Expression : undefined),
queryId: groupQueryId,
}).then(
(result) => {
setCurrentFilterId(filterId);

if (!result.data) return;

const ids = result.data.pages[page].map(group => group.IDs.ID);
Expand All @@ -120,7 +158,18 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
.then(result => result.data && setSeriesData(result.data))
.catch(error => console.error(error)).finally(() => setFetchingPage(false));
}
}, 200), [groupId, fetchGroups, pageSize, filterId, showRandomPoster, groupQueryId, fetchGroupExtras, fetchSeries]);
}, 200), [
groupId,
fetchGroups,
pageSize,
showRandomPoster,
searchQuery,
filterId,
filterQuery.data?.Expression,
groupQueryId,
fetchGroupExtras,
fetchSeries,
]);

useEffect(() => {
fetchPage.cancel();
Expand All @@ -129,8 +178,10 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
let shouldFetch: boolean;
if (groupId) {
shouldFetch = true;
} else if (searchQuery !== currentSearch) {
setCurrentSearch(searchQuery);
shouldFetch = true;
} else if (filterId !== currentFilterId) {
setCurrentFilterId(filterId);
shouldFetch = true;
} else {
shouldFetch = groupsData.isUninitialized;
Expand All @@ -143,7 +194,7 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
return () => fetchPage.cancel();
// TODO: Figure out how to do it better
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filterId, groupId, groupsData.isUninitialized, fetchPage]);
}, [filterId, groupId, groupsData.isUninitialized, fetchPage, searchQuery]);

useEffect(() => {
if (!groupId) setSeriesData({ pages: [], total: -1 });
Expand Down Expand Up @@ -181,7 +232,7 @@ const CollectionView = ({ isSidebarOpen, mode, setGroupTotal, setTimelineSeries
{/* This is always equal width to the actual grid container so we are using the ref here */}
{/* Otherwise we would need two refs to remove flicker */}
<div className="flex w-full justify-center" ref={gridContainerRef}>
{isLoading || seriesData.total === -1
{isLoading || (groupId && seriesData.total === -1)
? <Icon path={mdiLoading} size={3} className="text-panel-text-primary" spin />
: 'No series/groups available!'}
</div>
Expand Down
14 changes: 14 additions & 0 deletions src/core/buildFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { FilterCondition } from '@/core/types/api/filter';

const buildFilter = (filters: FilterCondition[]) => {
if (filters.length > 1) {
return {
Type: 'And',
Left: filters[0],
Right: buildFilter(filters.slice(1)),
};
}
return filters[0];
};

export default buildFilter;
4 changes: 0 additions & 4 deletions src/core/rtkQuery/splitV3Api/collectionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,10 @@ const collectionApi = splitV3Api.injectEndpoints({
params: { includeEmpty, topLevelOnly },
}),
}),
getFilter: build.query<CollectionFilterType, { filterId?: string }>({
query: ({ filterId }) => ({ url: `Filter/${filterId}` }),
}),
}),
});

export const {
useGetFilterQuery,
useGetGroupQuery,
useLazyGetFiltersQuery,
useLazyGetGroupSeriesQuery,
Expand Down
60 changes: 60 additions & 0 deletions src/core/rtkQuery/splitV3Api/filterApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { defaultSerializeQueryArgs } from '@reduxjs/toolkit/query';
import { omit } from 'lodash';

import { splitV3Api } from '@/core/rtkQuery/splitV3Api';

import type { InfiniteResultType, ListResultType, PaginationType } from '@/core/types/api';
import type { CollectionGroupType } from '@/core/types/api/collection';
import type { FilterType } from '@/core/types/api/filter';

const filterApi = splitV3Api.injectEndpoints({
endpoints: build => ({
getFilter: build.query<FilterType, { filterId: string }>({
query: ({ filterId }) => ({
url: `Filter/${filterId}`,
params: {
withConditions: true,
},
}),
}),
getFilteredGroups: build.query<
InfiniteResultType<CollectionGroupType[]>,
PaginationType & { randomImages?: boolean, filterCriteria: FilterType, queryId: number }
>({
query: ({ filterCriteria, queryId: _, ...params }) => ({
url: 'Filter/Preview/Group',
method: 'POST',
params,
body: filterCriteria,
}),
transformResponse: (response: ListResultType<CollectionGroupType[]>, _, args) => ({
pages: {
[args.page ?? 1]: response.List,
},
total: response.Total,
}),
// Only have one cache entry because the arg always maps to one string
serializeQueryArgs: ({ endpointDefinition, endpointName, queryArgs }) =>
defaultSerializeQueryArgs({
endpointName,
queryArgs: omit(queryArgs, ['page']),
endpointDefinition,
}),
// Always merge incoming data to the cache entry
merge: (currentCache, newItems) => {
const tempCache = { ...currentCache };
tempCache.pages = { ...currentCache.pages, ...newItems.pages };
return tempCache;
},
// Refetch when the page arg changes
forceRefetch({ currentArg, previousArg }) {
return currentArg !== previousArg;
},
}),
}),
});

export const {
useGetFilterQuery,
useLazyGetFilteredGroupsQuery,
} = filterApi;
62 changes: 62 additions & 0 deletions src/core/types/api/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
type ExpressionType =
| 'And'
| 'Not'
| 'Or'
| 'HasUnwatchedEpisodes'
| 'HasWatchedEpisodes'
| 'NameSelector'
| 'StringContains'
| 'StringEndsWith'
| 'StringEquals'
| 'StringFuzzyMatches'
| 'StringNotEquals'
| 'StringRegexMatches'
| 'StringStartsWith';

type SortingType =
| 'AddedDate'
| 'AirDate'
| 'AudioLanguageCount'
| 'AverageAniDBRating'
| 'EpisodeCount'
| 'HighestAniDBRating'
| 'HighestUserRating'
| 'LastAddedDate'
| 'LastAirDate'
| 'LastWatchedDate'
| 'LowestAniDBRating'
| 'LowestUserRating'
| 'MissingEpisodeCollectingCount'
| 'MissingEpisodeCount'
| 'Name'
| 'SeriesCount'
| 'SortingName'
| 'SubtitleLanguageCount'
| 'TotalEpisodeCount'
| 'UnwatchedEpisodeCount'
| 'WatchedDate'
| 'WatchedEpisodeCount';

export type FilterCondition = {
Type: ExpressionType;
Left?: FilterCondition;
Right?: FilterCondition;
Parameter?: string;
SecondParameter?: string;
};

type SortingCriteria = {
Type: SortingType;
Next?: SortingCriteria;
IsInverted: boolean;
};

export type FilterType = {
Name?: string;
ParentID?: number;
IsDirectory?: boolean;
IsHidden?: boolean;
ApplyAtSeriesLevel?: boolean;
Expression?: FilterCondition;
SortingCriteria?: SortingCriteria;
};
Loading

0 comments on commit 5118d3c

Please sign in to comment.