Add library filter for trackers
This commit is contained in:
@@ -31,6 +31,7 @@ export const DefaultLibraryOptions: LibraryOptions = {
|
|||||||
sortDesc: undefined,
|
sortDesc: undefined,
|
||||||
sorts: undefined,
|
sorts: undefined,
|
||||||
unread: undefined,
|
unread: undefined,
|
||||||
|
tracker: {},
|
||||||
|
|
||||||
showTabSize: false,
|
showTabSize: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import { SortRadioInput } from '@/components/atoms/SortRadioInput';
|
|||||||
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput';
|
import { ThreeStateCheckboxInput } from '@/components/atoms/ThreeStateCheckboxInput';
|
||||||
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||||
import { OptionsTabs } from '@/components/molecules/OptionsTabs';
|
import { OptionsTabs } from '@/components/molecules/OptionsTabs';
|
||||||
|
import { requestManager } from '@/lib/requests/RequestManager.ts';
|
||||||
|
import { Trackers } from '@/lib/data/Trackers.ts';
|
||||||
|
|
||||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||||
filter: 'global.label.filter',
|
filter: 'global.label.filter',
|
||||||
@@ -41,6 +43,9 @@ export const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { options, setOptions } = useLibraryOptionsContext();
|
const { options, setOptions } = useLibraryOptionsContext();
|
||||||
|
|
||||||
|
const trackerList = requestManager.useGetTrackerList();
|
||||||
|
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
|
||||||
|
|
||||||
const handleFilterChange = <T extends keyof LibraryOptions>(key: T, value: LibraryOptions[T]) => {
|
const handleFilterChange = <T extends keyof LibraryOptions>(key: T, value: LibraryOptions[T]) => {
|
||||||
setOptions((v) => ({ ...v, [key]: value }));
|
setOptions((v) => ({ ...v, [key]: value }));
|
||||||
};
|
};
|
||||||
@@ -65,6 +70,17 @@ export const LibraryOptionsPanel: React.FC<IProps> = ({ open, onClose }) => {
|
|||||||
checked={options.downloaded}
|
checked={options.downloaded}
|
||||||
onChange={(c) => handleFilterChange('downloaded', c)}
|
onChange={(c) => handleFilterChange('downloaded', c)}
|
||||||
/>
|
/>
|
||||||
|
<FormLabel sx={{ mt: 2 }}>{t('global.grid_layout.title')}</FormLabel>
|
||||||
|
{loggedInTrackers.map((tracker) => (
|
||||||
|
<ThreeStateCheckboxInput
|
||||||
|
key={tracker.id}
|
||||||
|
label={tracker.name}
|
||||||
|
checked={options.tracker[tracker.id]}
|
||||||
|
onChange={(checked) =>
|
||||||
|
handleFilterChange('tracker', { ...options.tracker, [tracker.id]: checked })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ interface IProps {
|
|||||||
export const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
|
export const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
|
||||||
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DefaultLibraryOptions);
|
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', DefaultLibraryOptions);
|
||||||
|
|
||||||
const value = useMemo(() => ({ options, setOptions }), [options, setOptions]);
|
const value = useMemo(
|
||||||
|
() => ({ options: { ...DefaultLibraryOptions, ...options }, setOptions }),
|
||||||
|
[options, setOptions],
|
||||||
|
);
|
||||||
|
|
||||||
return <LibraryOptionsContext.Provider value={value}>{children}</LibraryOptionsContext.Provider>;
|
return <LibraryOptionsContext.Provider value={value}>{children}</LibraryOptionsContext.Provider>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,10 @@
|
|||||||
|
|
||||||
import { StringParam, useQueryParam } from 'use-query-params';
|
import { StringParam, useQueryParam } from 'use-query-params';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { LibrarySortMode, NullAndUndefined, TManga } from '@/typings.ts';
|
import { LibraryOptions, LibrarySortMode, NullAndUndefined, TManga } from '@/typings.ts';
|
||||||
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx';
|
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx';
|
||||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||||
|
import { Trackers } from '@/lib/data/Trackers.ts';
|
||||||
|
|
||||||
const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: TManga): boolean => {
|
const unreadFilter = (unread: NullAndUndefined<boolean>, { unreadCount }: TManga): boolean => {
|
||||||
switch (unread) {
|
switch (unread) {
|
||||||
@@ -45,18 +46,37 @@ const queryGenreFilter = (query: NullAndUndefined<string>, { genre }: TManga): b
|
|||||||
return queries.every((element) => genre.map((el) => el.toLowerCase()).includes(element));
|
return queries.every((element) => genre.map((el) => el.toLowerCase()).includes(element));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const trackerFilter = (trackFilters: LibraryOptions['tracker'], manga: TManga): boolean =>
|
||||||
|
Object.entries(trackFilters)
|
||||||
|
.map(([trackFilterId, trackFilterState]) => {
|
||||||
|
const mangaTrackers = Trackers.getTrackers(manga.trackRecords.nodes);
|
||||||
|
const isTrackerBound = mangaTrackers.some((tracker) => tracker.id === Number(trackFilterId));
|
||||||
|
|
||||||
|
switch (trackFilterState) {
|
||||||
|
case true:
|
||||||
|
return isTrackerBound;
|
||||||
|
case false:
|
||||||
|
return !isTrackerBound;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.every((matchesFilter) => matchesFilter);
|
||||||
|
|
||||||
const filterManga = (
|
const filterManga = (
|
||||||
mangas: TManga[],
|
mangas: TManga[],
|
||||||
query: NullAndUndefined<string>,
|
query: NullAndUndefined<string>,
|
||||||
unread: NullAndUndefined<boolean>,
|
unread: NullAndUndefined<boolean>,
|
||||||
downloaded: NullAndUndefined<boolean>,
|
downloaded: NullAndUndefined<boolean>,
|
||||||
|
tracker: LibraryOptions['tracker'],
|
||||||
ignoreFilters: boolean,
|
ignoreFilters: boolean,
|
||||||
): TManga[] =>
|
): TManga[] =>
|
||||||
mangas.filter((manga) => {
|
mangas.filter((manga) => {
|
||||||
const ignoreFiltersWhileSearching = ignoreFilters && query?.length;
|
const ignoreFiltersWhileSearching = ignoreFilters && query?.length;
|
||||||
const matchesSearch = queryFilter(query, manga) || queryGenreFilter(query, manga);
|
const matchesSearch = queryFilter(query, manga) || queryGenreFilter(query, manga);
|
||||||
const matchesFilters =
|
const matchesFilters =
|
||||||
ignoreFiltersWhileSearching || (downloadedFilter(downloaded, manga) && unreadFilter(unread, manga));
|
ignoreFiltersWhileSearching ||
|
||||||
|
(downloadedFilter(downloaded, manga) && unreadFilter(unread, manga) && trackerFilter(tracker, manga));
|
||||||
|
|
||||||
return matchesSearch && matchesFilters;
|
return matchesSearch && matchesFilters;
|
||||||
});
|
});
|
||||||
@@ -116,12 +136,12 @@ const sortManga = (
|
|||||||
export const useGetVisibleLibraryMangas = (mangas: TManga[]) => {
|
export const useGetVisibleLibraryMangas = (mangas: TManga[]) => {
|
||||||
const [query] = useQueryParam('query', StringParam);
|
const [query] = useQueryParam('query', StringParam);
|
||||||
const { options } = useLibraryOptionsContext();
|
const { options } = useLibraryOptionsContext();
|
||||||
const { unread, downloaded } = options;
|
const { unread, downloaded, tracker } = options;
|
||||||
const { settings } = useMetadataServerSettings();
|
const { settings } = useMetadataServerSettings();
|
||||||
|
|
||||||
const filteredMangas = useMemo(
|
const filteredMangas = useMemo(
|
||||||
() => filterManga(mangas, query, unread, downloaded, settings.ignoreFilters),
|
() => filterManga(mangas, query, unread, downloaded, tracker, settings.ignoreFilters),
|
||||||
[mangas, query, unread, downloaded, settings.ignoreFilters],
|
[mangas, query, unread, downloaded, tracker, settings.ignoreFilters],
|
||||||
);
|
);
|
||||||
const sortedMangas = useMemo(
|
const sortedMangas = useMemo(
|
||||||
() => sortManga(filteredMangas, options.sorts, options.sortDesc),
|
() => sortManga(filteredMangas, options.sorts, options.sortDesc),
|
||||||
|
|||||||
@@ -354,6 +354,7 @@
|
|||||||
"label": {
|
"label": {
|
||||||
"bookmarked": "Bookmarked",
|
"bookmarked": "Bookmarked",
|
||||||
"downloaded": "Downloaded",
|
"downloaded": "Downloaded",
|
||||||
|
"tracked": "Tracked",
|
||||||
"unread": "Unread"
|
"unread": "Unread"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
MetaType,
|
MetaType,
|
||||||
SourcePreferenceChangeInput,
|
SourcePreferenceChangeInput,
|
||||||
} from '@/lib/graphql/generated/graphql.ts';
|
} from '@/lib/graphql/generated/graphql.ts';
|
||||||
|
import { TBaseTracker } from '@/lib/data/Trackers.ts';
|
||||||
|
|
||||||
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
|
type GenericLocation<State = any> = Omit<Location, 'state'> & { state?: State };
|
||||||
|
|
||||||
@@ -391,6 +392,7 @@ export interface LibraryOptions {
|
|||||||
sorts: NullAndUndefined<LibrarySortMode>;
|
sorts: NullAndUndefined<LibrarySortMode>;
|
||||||
sortDesc: NullAndUndefined<boolean>;
|
sortDesc: NullAndUndefined<boolean>;
|
||||||
showTabSize: boolean;
|
showTabSize: boolean;
|
||||||
|
tracker: Record<TBaseTracker['id'], NullAndUndefined<boolean>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UpdateCheck = {
|
export type UpdateCheck = {
|
||||||
|
|||||||
Reference in New Issue
Block a user