2023-12-25 21:37:45 +01:00
|
|
|
/*
|
|
|
|
|
* Copyright (C) Contributors to the Suwayomi project
|
|
|
|
|
*
|
|
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { StringParam, useQueryParam } from 'use-query-params';
|
|
|
|
|
import { useMemo } from 'react';
|
2024-07-08 18:02:50 +02:00
|
|
|
import { LibraryOptions, LibrarySortMode, NullAndUndefined } from '@/typings.ts';
|
2023-12-25 21:37:45 +01:00
|
|
|
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx';
|
2024-03-16 16:21:15 +01:00
|
|
|
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
2024-07-08 18:02:50 +02:00
|
|
|
import { ChapterType, MangaType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
|
|
|
|
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
|
2023-12-25 21:37:45 +01:00
|
|
|
|
2024-05-03 17:01:04 +02:00
|
|
|
const triStateFilter = (
|
|
|
|
|
triState: NullAndUndefined<boolean>,
|
|
|
|
|
enabledFilter: () => boolean,
|
|
|
|
|
disabledFilter: () => boolean,
|
|
|
|
|
): boolean => {
|
|
|
|
|
switch (triState) {
|
2023-12-25 21:37:45 +01:00
|
|
|
case true:
|
2024-05-03 17:01:04 +02:00
|
|
|
return enabledFilter();
|
2023-12-25 21:37:45 +01:00
|
|
|
case false:
|
2024-05-03 17:01:04 +02:00
|
|
|
return disabledFilter();
|
2023-12-25 21:37:45 +01:00
|
|
|
default:
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-03 17:01:04 +02:00
|
|
|
const triStateFilterNumber = (triState: NullAndUndefined<boolean>, count?: number): boolean =>
|
|
|
|
|
triStateFilter(
|
|
|
|
|
triState,
|
|
|
|
|
() => !!count && count >= 1,
|
|
|
|
|
() => count === 0,
|
|
|
|
|
);
|
2024-04-01 01:12:26 +02:00
|
|
|
|
2024-07-08 18:02:50 +02:00
|
|
|
type TMangaQueryFilter = Pick<MangaType, 'title'>;
|
|
|
|
|
const queryFilter = (query: NullAndUndefined<string>, { title }: TMangaQueryFilter): boolean => {
|
2023-12-25 21:37:45 +01:00
|
|
|
if (!query) return true;
|
|
|
|
|
return title.toLowerCase().includes(query.toLowerCase());
|
|
|
|
|
};
|
|
|
|
|
|
2024-07-08 18:02:50 +02:00
|
|
|
type TMangaQueryGenreFilter = Pick<MangaType, 'genre'>;
|
|
|
|
|
const queryGenreFilter = (query: NullAndUndefined<string>, { genre }: TMangaQueryGenreFilter): boolean => {
|
2023-12-25 21:37:45 +01:00
|
|
|
if (!query) return true;
|
|
|
|
|
const queries = query.split(',').map((str) => str.toLowerCase().trim());
|
|
|
|
|
return queries.every((element) => genre.map((el) => el.toLowerCase()).includes(element));
|
|
|
|
|
};
|
|
|
|
|
|
2024-07-08 18:02:50 +02:00
|
|
|
type TMangaTrackerFilter = { trackRecords: { nodes: Pick<TrackRecordType, 'id' | 'trackerId'>[] } };
|
|
|
|
|
const trackerFilter = (trackFilters: LibraryOptions['tracker'], manga: TMangaTrackerFilter): boolean =>
|
2024-03-28 20:00:46 +01:00
|
|
|
Object.entries(trackFilters)
|
|
|
|
|
.map(([trackFilterId, trackFilterState]) => {
|
2024-07-08 18:02:50 +02:00
|
|
|
const isTrackerBound = manga.trackRecords.nodes.some(
|
|
|
|
|
(trackRecord) => trackRecord.trackerId === Number(trackFilterId),
|
|
|
|
|
);
|
2024-03-28 20:00:46 +01:00
|
|
|
|
2024-05-03 17:01:04 +02:00
|
|
|
return triStateFilter(
|
|
|
|
|
trackFilterState,
|
|
|
|
|
() => isTrackerBound,
|
|
|
|
|
() => !isTrackerBound,
|
|
|
|
|
);
|
2024-03-28 20:00:46 +01:00
|
|
|
})
|
|
|
|
|
.every((matchesFilter) => matchesFilter);
|
|
|
|
|
|
2024-07-08 18:02:50 +02:00
|
|
|
type TMangaFilter = TMangaQueryFilter &
|
|
|
|
|
TMangaQueryGenreFilter &
|
|
|
|
|
TMangaTrackerFilter &
|
|
|
|
|
Pick<MangaType, 'downloadCount' | 'unreadCount' | 'bookmarkCount'>;
|
|
|
|
|
const filterManga = <Manga extends TMangaFilter>(
|
|
|
|
|
mangas: Manga[],
|
2023-12-25 21:37:45 +01:00
|
|
|
query: NullAndUndefined<string>,
|
|
|
|
|
unread: NullAndUndefined<boolean>,
|
|
|
|
|
downloaded: NullAndUndefined<boolean>,
|
2024-04-01 01:12:26 +02:00
|
|
|
bookmarked: NullAndUndefined<boolean>,
|
2024-03-28 20:00:46 +01:00
|
|
|
tracker: LibraryOptions['tracker'],
|
2023-12-25 21:37:45 +01:00
|
|
|
ignoreFilters: boolean,
|
2024-07-08 18:02:50 +02:00
|
|
|
): Manga[] =>
|
2023-12-25 21:37:45 +01:00
|
|
|
mangas.filter((manga) => {
|
|
|
|
|
const ignoreFiltersWhileSearching = ignoreFilters && query?.length;
|
|
|
|
|
const matchesSearch = queryFilter(query, manga) || queryGenreFilter(query, manga);
|
|
|
|
|
const matchesFilters =
|
2024-03-28 20:00:46 +01:00
|
|
|
ignoreFiltersWhileSearching ||
|
2024-05-03 17:01:04 +02:00
|
|
|
(triStateFilterNumber(downloaded, manga.downloadCount) &&
|
|
|
|
|
triStateFilterNumber(unread, manga.unreadCount) &&
|
|
|
|
|
triStateFilterNumber(bookmarked, manga.bookmarkCount) &&
|
2024-04-01 01:12:26 +02:00
|
|
|
trackerFilter(tracker, manga));
|
2023-12-25 21:37:45 +01:00
|
|
|
|
|
|
|
|
return matchesSearch && matchesFilters;
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-03 17:01:04 +02:00
|
|
|
const sortByNumber = (a: number | string = 0, b: number | string = 0) => Number(a) - Number(b);
|
2024-01-11 03:56:04 +01:00
|
|
|
|
2024-05-03 17:01:04 +02:00
|
|
|
const sortByString = (a: string, b: string): number => a.localeCompare(b);
|
2024-01-11 03:56:04 +01:00
|
|
|
|
2024-07-08 18:02:50 +02:00
|
|
|
type TMangaSort = Pick<MangaType, 'title' | 'inLibraryAt' | 'unreadCount'> & {
|
|
|
|
|
lastReadChapter?: Pick<ChapterType, 'lastReadAt'> | null;
|
|
|
|
|
latestUploadedChapter?: Pick<ChapterType, 'uploadDate'> | null;
|
|
|
|
|
latestFetchedChapter?: Pick<ChapterType, 'fetchedAt'> | null;
|
|
|
|
|
};
|
|
|
|
|
const sortManga = <Manga extends TMangaSort>(
|
|
|
|
|
manga: Manga[],
|
2023-12-25 21:37:45 +01:00
|
|
|
sort: NullAndUndefined<LibrarySortMode>,
|
|
|
|
|
desc: NullAndUndefined<boolean>,
|
2024-07-08 18:02:50 +02:00
|
|
|
): Manga[] => {
|
2023-12-25 21:37:45 +01:00
|
|
|
const result = [...manga];
|
|
|
|
|
|
|
|
|
|
switch (sort) {
|
|
|
|
|
case 'sortAlph':
|
2024-05-03 17:01:04 +02:00
|
|
|
result.sort((a, b) => sortByString(a.title, b.title));
|
2023-12-25 21:37:45 +01:00
|
|
|
break;
|
|
|
|
|
case 'sortDateAdded':
|
2024-05-03 17:01:04 +02:00
|
|
|
result.sort((a, b) => sortByNumber(a.inLibraryAt, b.inLibraryAt));
|
2023-12-25 21:37:45 +01:00
|
|
|
break;
|
|
|
|
|
case 'sortToRead':
|
2024-05-03 17:01:04 +02:00
|
|
|
result.sort((a, b) => sortByNumber(a.unreadCount, b.unreadCount));
|
2023-12-25 21:37:45 +01:00
|
|
|
break;
|
|
|
|
|
case 'sortLastRead':
|
2024-05-03 17:01:04 +02:00
|
|
|
result.sort((a, b) => sortByNumber(a.lastReadChapter?.lastReadAt, b.lastReadChapter?.lastReadAt));
|
2023-12-25 21:37:45 +01:00
|
|
|
break;
|
2024-01-11 03:56:04 +01:00
|
|
|
case 'sortLatestUploadedChapter':
|
2024-05-03 17:01:04 +02:00
|
|
|
result.sort((a, b) =>
|
|
|
|
|
sortByNumber(a.latestUploadedChapter?.uploadDate, b.latestUploadedChapter?.uploadDate),
|
|
|
|
|
);
|
2024-01-11 03:56:04 +01:00
|
|
|
break;
|
|
|
|
|
case 'sortLatestFetchedChapter':
|
2024-05-03 17:01:04 +02:00
|
|
|
result.sort((a, b) => sortByNumber(a.latestFetchedChapter?.fetchedAt, b.latestFetchedChapter?.fetchedAt));
|
2024-01-11 03:56:04 +01:00
|
|
|
break;
|
2023-12-25 21:37:45 +01:00
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-03 17:01:04 +02:00
|
|
|
if (desc) {
|
2023-12-25 21:37:45 +01:00
|
|
|
result.reverse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
2024-07-08 18:02:50 +02:00
|
|
|
export const useGetVisibleLibraryMangas = <Manga extends MangaIdInfo & TMangaFilter & TMangaSort>(
|
|
|
|
|
mangas: Manga[],
|
|
|
|
|
): {
|
|
|
|
|
visibleMangas: Manga[];
|
|
|
|
|
showFilteredOutMessage: boolean;
|
|
|
|
|
} => {
|
2023-12-25 21:37:45 +01:00
|
|
|
const [query] = useQueryParam('query', StringParam);
|
|
|
|
|
const { options } = useLibraryOptionsContext();
|
2024-04-01 01:12:26 +02:00
|
|
|
const { unread, downloaded, bookmarked, tracker } = options;
|
2023-12-27 18:08:16 +01:00
|
|
|
const { settings } = useMetadataServerSettings();
|
2023-12-25 21:37:45 +01:00
|
|
|
|
|
|
|
|
const filteredMangas = useMemo(
|
2024-04-01 01:12:26 +02:00
|
|
|
() => filterManga(mangas, query, unread, downloaded, bookmarked, tracker, settings.ignoreFilters),
|
|
|
|
|
[mangas, query, unread, downloaded, bookmarked, tracker, settings.ignoreFilters],
|
2023-12-25 21:37:45 +01:00
|
|
|
);
|
|
|
|
|
const sortedMangas = useMemo(
|
|
|
|
|
() => sortManga(filteredMangas, options.sorts, options.sortDesc),
|
|
|
|
|
[filteredMangas, options.sorts, options.sortDesc],
|
|
|
|
|
);
|
|
|
|
|
|
2024-03-30 16:07:29 +01:00
|
|
|
const isATrackFilterActive = Object.values(options.tracker).some((trackFilterState) => trackFilterState != null);
|
2023-12-25 21:37:45 +01:00
|
|
|
const showFilteredOutMessage =
|
2024-04-01 01:12:26 +02:00
|
|
|
(unread != null || downloaded != null || bookmarked != null || !!query || isATrackFilterActive) &&
|
2024-03-30 16:07:29 +01:00
|
|
|
filteredMangas.length === 0 &&
|
|
|
|
|
mangas.length > 0;
|
2023-12-25 21:37:45 +01:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
visibleMangas: sortedMangas,
|
|
|
|
|
showFilteredOutMessage,
|
|
|
|
|
};
|
|
|
|
|
};
|