Files
suwayomi-material-you-webui/src/components/library/useGetVisibleLibraryMangas.ts

250 lines
9.1 KiB
TypeScript
Raw Normal View History

/*
* 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';
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx';
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
import { ChapterType, MangaType, SourceType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
2024-07-08 18:02:50 +02:00
import { MangaIdInfo } from '@/lib/data/Mangas.ts';
import { baseCleanup, enhancedCleanup } from '@/lib/data/Strings.ts';
const triStateFilter = (
triState: NullAndUndefined<boolean>,
enabledFilter: () => boolean,
disabledFilter: () => boolean,
): boolean => {
switch (triState) {
case true:
return enabledFilter();
case false:
return disabledFilter();
default:
return true;
}
};
const triStateFilterNumber = (triState: NullAndUndefined<boolean>, count?: number): boolean =>
triStateFilter(
triState,
() => !!count && count >= 1,
() => count === 0,
);
const triStateFilterBoolean = (triState: NullAndUndefined<boolean>, status?: boolean): boolean =>
triStateFilter(
triState,
() => !!status,
() => !status,
);
2024-07-08 18:02:50 +02:00
type TMangaQueryFilter = Pick<MangaType, 'title'>;
const queryFilter = (query: NullAndUndefined<string>, { title }: TMangaQueryFilter): boolean => {
if (!query) return true;
2024-08-14 15:20:59 +02:00
const cleanedUpQuery = baseCleanup(query);
2024-08-14 15:20:59 +02:00
return baseCleanup(title).includes(cleanedUpQuery) || enhancedCleanup(title).includes(cleanedUpQuery);
};
2024-07-08 18:02:50 +02:00
type TMangaQueryGenreFilter = Pick<MangaType, 'genre'>;
const queryGenreFilter = (query: NullAndUndefined<string>, { genre: mangaGenres }: TMangaQueryGenreFilter): boolean => {
if (!query) return true;
const genreQueries = query.split(',').map(baseCleanup);
const genres = mangaGenres
.map((genre) => {
const baseCleanedUpGenre = baseCleanup(genre);
const fullyCleanedUpGenre = enhancedCleanup(baseCleanedUpGenre);
return `${baseCleanedUpGenre}, ${fullyCleanedUpGenre}`;
})
.join(', ');
return genreQueries.every((genreQuery) => genres.includes(genreQuery));
};
2024-08-14 15:20:59 +02:00
type TMangaDescriptionFilter = Pick<MangaType, 'description'>;
const queryDescriptionFilter = (query: NullAndUndefined<string>, { description }: TMangaDescriptionFilter): boolean => {
if (!query) return true;
if (!description) return false;
const cleanedUpQuery = baseCleanup(query);
return baseCleanup(description).includes(cleanedUpQuery) || enhancedCleanup(description).includes(cleanedUpQuery);
};
type TMangaArtistFilter = Pick<MangaType, 'artist'>;
const queryArtistFilter = (query: NullAndUndefined<string>, { artist }: TMangaArtistFilter): boolean => {
if (!query) return true;
if (!artist) return false;
return enhancedCleanup(artist).includes(enhancedCleanup(query));
};
type TMangaAuthorFilter = Pick<MangaType, 'author'>;
const queryAuthorFilter = (query: NullAndUndefined<string>, { author }: TMangaAuthorFilter): boolean => {
if (!query) return true;
if (!author) return false;
return enhancedCleanup(author).includes(enhancedCleanup(query));
};
type TMangaSourceFilter = { source?: NullAndUndefined<Pick<SourceType, 'displayName'>> };
const querySourceFilter = (query: NullAndUndefined<string>, { source }: TMangaSourceFilter): boolean => {
if (!query) return true;
if (!source) return false;
return enhancedCleanup(source.displayName).includes(enhancedCleanup(query));
};
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
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 &
2024-08-14 15:20:59 +02:00
TMangaDescriptionFilter &
TMangaArtistFilter &
TMangaAuthorFilter &
TMangaSourceFilter &
Pick<MangaType, 'downloadCount' | 'unreadCount' | 'bookmarkCount' | 'hasDuplicateChapters'>;
2024-07-08 18:02:50 +02:00
const filterManga = <Manga extends TMangaFilter>(
mangas: Manga[],
query: NullAndUndefined<string>,
unread: NullAndUndefined<boolean>,
downloaded: NullAndUndefined<boolean>,
bookmarked: NullAndUndefined<boolean>,
hasDuplicateChapters: NullAndUndefined<boolean>,
2024-03-28 20:00:46 +01:00
tracker: LibraryOptions['tracker'],
ignoreFilters: boolean,
2024-07-08 18:02:50 +02:00
): Manga[] =>
mangas.filter((manga) => {
const ignoreFiltersWhileSearching = ignoreFilters && query?.length;
2024-08-14 15:20:59 +02:00
const matchesSearch =
queryFilter(query, manga) ||
queryGenreFilter(query, manga) ||
queryDescriptionFilter(query, manga) ||
queryArtistFilter(query, manga) ||
queryAuthorFilter(query, manga) ||
querySourceFilter(query, manga);
const matchesFilters =
2024-03-28 20:00:46 +01:00
ignoreFiltersWhileSearching ||
(triStateFilterNumber(downloaded, manga.downloadCount) &&
triStateFilterNumber(unread, manga.unreadCount) &&
triStateFilterNumber(bookmarked, manga.bookmarkCount) &&
triStateFilterBoolean(hasDuplicateChapters, manga.hasDuplicateChapters) &&
trackerFilter(tracker, manga));
return matchesSearch && matchesFilters;
});
const sortByNumber = (a: number | string = 0, b: number | string = 0) => Number(a) - Number(b);
const sortByString = (a: string, b: string): number => a.localeCompare(b);
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[],
sort: NullAndUndefined<LibrarySortMode>,
desc: NullAndUndefined<boolean>,
2024-07-08 18:02:50 +02:00
): Manga[] => {
const result = [...manga];
switch (sort) {
case 'sortAlph':
result.sort((a, b) => sortByString(a.title, b.title));
break;
case 'sortDateAdded':
result.sort((a, b) => sortByNumber(a.inLibraryAt, b.inLibraryAt));
break;
case 'sortToRead':
result.sort((a, b) => sortByNumber(a.unreadCount, b.unreadCount));
break;
case 'sortLastRead':
result.sort((a, b) => sortByNumber(a.lastReadChapter?.lastReadAt, b.lastReadChapter?.lastReadAt));
break;
case 'sortLatestUploadedChapter':
result.sort((a, b) =>
sortByNumber(a.latestUploadedChapter?.uploadDate, b.latestUploadedChapter?.uploadDate),
);
break;
case 'sortLatestFetchedChapter':
result.sort((a, b) => sortByNumber(a.latestFetchedChapter?.fetchedAt, b.latestFetchedChapter?.fetchedAt));
break;
default:
break;
}
if (desc) {
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;
} => {
const [query] = useQueryParam('query', StringParam);
const { options } = useLibraryOptionsContext();
const { unread, downloaded, bookmarked, tracker, hasDuplicateChapters } = options;
const { settings } = useMetadataServerSettings();
const filteredMangas = useMemo(
() =>
filterManga(
mangas,
query,
unread,
downloaded,
bookmarked,
hasDuplicateChapters,
tracker,
settings.ignoreFilters,
),
[mangas, query, unread, downloaded, bookmarked, hasDuplicateChapters, tracker, settings.ignoreFilters],
);
const sortedMangas = useMemo(
() => sortManga(filteredMangas, options.sorts, options.sortDesc),
[filteredMangas, options.sorts, options.sortDesc],
);
const isATrackFilterActive = Object.values(options.tracker).some((trackFilterState) => trackFilterState != null);
const showFilteredOutMessage =
(unread != null || downloaded != null || bookmarked != null || !!query || isATrackFilterActive) &&
filteredMangas.length === 0 &&
mangas.length > 0;
return {
visibleMangas: sortedMangas,
showFilteredOutMessage,
};
};