Move library files into new folder
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* 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 React, { useContext } from 'react';
|
||||
import { LibraryOptions } from '@/typings';
|
||||
import { getDefaultCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
|
||||
type ContextType = {
|
||||
options: LibraryOptions;
|
||||
setOptions: React.Dispatch<React.SetStateAction<LibraryOptions>>;
|
||||
};
|
||||
|
||||
export enum GridLayout {
|
||||
Compact = 0,
|
||||
Comfortable = 1,
|
||||
List = 2,
|
||||
}
|
||||
|
||||
export const LibraryOptionsContext = React.createContext<ContextType>({
|
||||
options: getDefaultCategoryMetadata(),
|
||||
setOptions: () => {},
|
||||
});
|
||||
|
||||
export function useLibraryOptionsContext() {
|
||||
return useContext(LibraryOptionsContext);
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
/*
|
||||
* 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 React, { useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GridLayout, useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||
import { IMangaGridProps, MangaGrid } from '@/modules/manga/components/MangaGrid';
|
||||
|
||||
interface LibraryMangaGridProps
|
||||
extends Required<Pick<IMangaGridProps, 'isSelectModeActive' | 'selectedMangaIds' | 'handleSelection' | 'mangas'>>,
|
||||
Pick<IMangaGridProps, 'retry' | 'message' | 'messageExtra'> {
|
||||
showFilteredOutMessage: boolean;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export const LibraryMangaGrid: React.FC<LibraryMangaGridProps> = ({
|
||||
showFilteredOutMessage,
|
||||
message,
|
||||
messageExtra,
|
||||
...gridProps
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { options } = useLibraryOptionsContext();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.body.style.overflowY = options.gridLayout === GridLayout.List ? 'auto' : 'scroll';
|
||||
return () => {
|
||||
document.body.style.overflowY = 'auto';
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MangaGrid
|
||||
gridWrapperProps={{ sx: { p: 1 } }}
|
||||
{...gridProps}
|
||||
hasNextPage={false}
|
||||
loadMore={() => undefined}
|
||||
message={showFilteredOutMessage ? t('library.error.label.no_matches') : message}
|
||||
messageExtra={showFilteredOutMessage ? undefined : messageExtra}
|
||||
gridLayout={options.gridLayout}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,216 +0,0 @@
|
||||
/*
|
||||
* 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 FormLabel from '@mui/material/FormLabel';
|
||||
import RadioGroup from '@mui/material/RadioGroup';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LibrarySortMode } from '@/typings';
|
||||
import { CheckboxInput } from '@/modules/core/components/inputs/CheckboxInput.tsx';
|
||||
import { RadioInput } from '@/modules/core/components/inputs/RadioInput.tsx';
|
||||
import { SortRadioInput } from '@/modules/core/components/inputs/SortRadioInput.tsx';
|
||||
import { ThreeStateCheckboxInput } from '@/modules/core/components/inputs/ThreeStateCheckboxInput.tsx';
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||
import { OptionsTabs } from '@/modules/core/components/OptionsTabs.tsx';
|
||||
import { requestManager } from '@/lib/requests/requests/RequestManager.ts';
|
||||
import { Trackers } from '@/lib/data/Trackers.ts';
|
||||
import { GetTrackersSettingsQuery, MangaStatus } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { GET_TRACKERS_SETTINGS } from '@/lib/graphql/queries/TrackerQuery.ts';
|
||||
import { statusToTranslationKey } from '@/modules/manga/services/Mangas.ts';
|
||||
import { CategoryMetadataInfo } from '@/lib/data/Categories.ts';
|
||||
import { createUpdateCategoryMetadata, getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { makeToast } from '@/lib/ui/Toast.ts';
|
||||
import {
|
||||
createUpdateMetadataServerSettings,
|
||||
useMetadataServerSettings,
|
||||
} from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { TranslationKey } from '@/Base.types.ts';
|
||||
|
||||
const TITLES: { [key in 'filter' | 'sort' | 'display']: TranslationKey } = {
|
||||
filter: 'global.label.filter',
|
||||
sort: 'global.label.sort',
|
||||
display: 'global.label.display',
|
||||
};
|
||||
|
||||
const SORT_OPTIONS: [LibrarySortMode, TranslationKey][] = [
|
||||
['unreadChapters', 'library.option.sort.label.by_unread_chapters'],
|
||||
['totalChapters', 'library.option.sort.label.by_total_chapters'],
|
||||
['alphabetically', 'library.option.sort.label.alphabetically'],
|
||||
['dateAdded', 'library.option.sort.label.by_date_added'],
|
||||
['lastRead', 'library.option.sort.label.by_last_read'],
|
||||
['latestFetchedChapter', 'library.option.sort.label.by_latest_fetched_chapter'],
|
||||
['latestUploadedChapter', 'library.option.sort.label.by_latest_uploaded_chapter'],
|
||||
];
|
||||
|
||||
export const LibraryOptionsPanel = ({
|
||||
category,
|
||||
open,
|
||||
onClose,
|
||||
}: {
|
||||
category: CategoryMetadataInfo;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const trackerList = requestManager.useGetTrackerList<GetTrackersSettingsQuery>(GET_TRACKERS_SETTINGS);
|
||||
const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? []);
|
||||
|
||||
const categoryLibraryOptions = useMemo(() => getCategoryMetadata(category), [category]);
|
||||
const updateCategoryLibraryOptions = createUpdateCategoryMetadata(category, () =>
|
||||
makeToast(t('global.error.label.failed_to_save_changes', 'error')),
|
||||
);
|
||||
|
||||
const {
|
||||
settings: { showTabSize },
|
||||
} = useMetadataServerSettings();
|
||||
const setSettingValue = createUpdateMetadataServerSettings<'showTabSize'>(() =>
|
||||
makeToast(t('search.error.label.failed_to_save_settings'), 'warning'),
|
||||
);
|
||||
|
||||
return (
|
||||
<OptionsTabs<'filter' | 'sort' | 'display'>
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
tabs={['filter', 'sort', 'display']}
|
||||
tabTitle={(key) => t(TITLES[key])}
|
||||
tabContent={(key) => {
|
||||
if (key === 'filter') {
|
||||
return (
|
||||
<>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.unread')}
|
||||
checked={categoryLibraryOptions.hasUnreadChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasUnreadChapters', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.downloaded')}
|
||||
checked={categoryLibraryOptions.hasDownloadedChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasDownloadedChapters', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.bookmarked')}
|
||||
checked={categoryLibraryOptions.hasBookmarkedChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasBookmarkedChapters', c)}
|
||||
/>
|
||||
<ThreeStateCheckboxInput
|
||||
label={t('global.filter.label.duplicate_chapters')}
|
||||
checked={categoryLibraryOptions.hasDuplicateChapters}
|
||||
onChange={(c) => updateCategoryLibraryOptions('hasDuplicateChapters', c)}
|
||||
/>
|
||||
<FormLabel sx={{ mt: 2 }}>{t('manga.label.status')}</FormLabel>
|
||||
{Object.values(MangaStatus).map((status) => (
|
||||
<ThreeStateCheckboxInput
|
||||
key={status}
|
||||
label={t(statusToTranslationKey[status])}
|
||||
checked={categoryLibraryOptions.hasStatus[status]}
|
||||
onChange={(checked) =>
|
||||
updateCategoryLibraryOptions('hasStatus', {
|
||||
...categoryLibraryOptions.hasStatus,
|
||||
[status]: checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<FormLabel sx={{ mt: 2 }}>{t('global.filter.label.tracked')}</FormLabel>
|
||||
{loggedInTrackers.map((tracker) => (
|
||||
<ThreeStateCheckboxInput
|
||||
key={tracker.id}
|
||||
label={tracker.name}
|
||||
checked={categoryLibraryOptions.hasTrackerBinding[tracker.id]}
|
||||
onChange={(checked) =>
|
||||
updateCategoryLibraryOptions('hasTrackerBinding', {
|
||||
...categoryLibraryOptions.hasTrackerBinding,
|
||||
[tracker.id]: checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (key === 'sort') {
|
||||
return SORT_OPTIONS.map(([mode, label]) => (
|
||||
<SortRadioInput
|
||||
key={mode}
|
||||
label={t(label)}
|
||||
checked={categoryLibraryOptions.sortBy === mode}
|
||||
sortDescending={categoryLibraryOptions.sortDesc}
|
||||
onClick={() =>
|
||||
mode !== categoryLibraryOptions.sortBy
|
||||
? updateCategoryLibraryOptions('sortBy', mode)
|
||||
: updateCategoryLibraryOptions('sortDesc', !categoryLibraryOptions.sortDesc)
|
||||
}
|
||||
/>
|
||||
));
|
||||
}
|
||||
if (key === 'display') {
|
||||
const { gridLayout, showContinueReadingButton, showDownloadBadge, showUnreadBadge } =
|
||||
categoryLibraryOptions;
|
||||
return (
|
||||
<>
|
||||
<FormLabel>{t('global.grid_layout.title')}</FormLabel>
|
||||
<RadioGroup
|
||||
onChange={(e) => updateCategoryLibraryOptions('gridLayout', Number(e.target.value))}
|
||||
value={gridLayout}
|
||||
>
|
||||
<RadioInput
|
||||
label={t('global.grid_layout.label.compact_grid')}
|
||||
value={GridLayout.Compact}
|
||||
checked={gridLayout == null || gridLayout === GridLayout.Compact}
|
||||
/>
|
||||
<RadioInput
|
||||
label={t('global.grid_layout.label.comfortable_grid')}
|
||||
value={GridLayout.Comfortable}
|
||||
checked={gridLayout === GridLayout.Comfortable}
|
||||
/>
|
||||
<RadioInput
|
||||
label={t('global.grid_layout.label.list')}
|
||||
value={GridLayout.List}
|
||||
checked={gridLayout === GridLayout.List}
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.badge.title')}</FormLabel>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.badge.label.unread_badges')}
|
||||
checked={showUnreadBadge}
|
||||
onChange={() => updateCategoryLibraryOptions('showUnreadBadge', !showUnreadBadge)}
|
||||
/>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.badge.label.download_badges')}
|
||||
checked={showDownloadBadge}
|
||||
onChange={() => updateCategoryLibraryOptions('showDownloadBadge', !showDownloadBadge)}
|
||||
/>
|
||||
|
||||
<FormLabel sx={{ mt: 2 }}>{t('library.option.display.tab.title')}</FormLabel>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.tab.label.show_number_of_items')}
|
||||
checked={showTabSize}
|
||||
onChange={() => setSettingValue('showTabSize', !showTabSize)}
|
||||
/>
|
||||
|
||||
<FormLabel sx={{ mt: 2 }}>{t('global.label.other')}</FormLabel>
|
||||
<CheckboxInput
|
||||
label={t('library.option.display.other.label.show_continue_reading_button')}
|
||||
checked={showContinueReadingButton}
|
||||
onChange={() =>
|
||||
updateCategoryLibraryOptions(
|
||||
'showContinueReadingButton',
|
||||
!showContinueReadingButton,
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,28 +0,0 @@
|
||||
/*
|
||||
* 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 React, { useMemo } from 'react';
|
||||
import { LibraryOptions } from '@/typings';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
import { LibraryOptionsContext } from '@/components/context/LibraryOptionsContext';
|
||||
import { getDefaultCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
|
||||
interface IProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const LibraryOptionsContextProvider: React.FC<IProps> = ({ children }) => {
|
||||
const [options, setOptions] = useLocalStorage<LibraryOptions>('libraryOptions', getDefaultCategoryMetadata());
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ options: { ...getDefaultCategoryMetadata(), ...options }, setOptions }),
|
||||
[options, setOptions],
|
||||
);
|
||||
|
||||
return <LibraryOptionsContext.Provider value={value}>{children}</LibraryOptionsContext.Provider>;
|
||||
};
|
||||
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* 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 FilterList from '@mui/icons-material/FilterList';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import { ComponentProps, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { LibraryOptionsPanel } from '@/components/library/LibraryOptionsPanel';
|
||||
import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext.tsx';
|
||||
|
||||
export const LibraryToolbarMenu = ({
|
||||
category,
|
||||
}: {
|
||||
category: ComponentProps<typeof LibraryOptionsPanel>['category'];
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const { options } = useLibraryOptionsContext();
|
||||
const active =
|
||||
options.hasDownloadedChapters != null ||
|
||||
options.hasUnreadChapters != null ||
|
||||
options.hasBookmarkedChapters != null ||
|
||||
options.hasDuplicateChapters != null ||
|
||||
Object.values(options.hasStatus).some((hasStatus) => hasStatus != null) ||
|
||||
Object.values(options.hasTrackerBinding).some((trackerFilterStatus) => trackerFilterStatus != null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Tooltip title={t('settings.title')}>
|
||||
<IconButton onClick={() => setOpen(!open)} color={active ? 'warning' : 'inherit'}>
|
||||
<FilterList />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<LibraryOptionsPanel category={category} open={open} onClose={() => setOpen(false)} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,259 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
import { LibraryOptions, LibrarySortMode } from '@/typings.ts';
|
||||
import { useMetadataServerSettings } from '@/lib/metadata/metadataServerSettings.ts';
|
||||
import { ChapterType, MangaType, SourceType, TrackRecordType } from '@/lib/graphql/generated/graphql.ts';
|
||||
import { MangaChapterCountInfo, MangaIdInfo } from '@/modules/manga/services/Mangas.ts';
|
||||
import { enhancedCleanup } from '@/lib/data/Strings.ts';
|
||||
import { CategoryMetadataInfo } from '@/lib/data/Categories.ts';
|
||||
import { getCategoryMetadata } from '@/lib/metadata/categoryMetadata.ts';
|
||||
import { NullAndUndefined } from '@/Base.types.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,
|
||||
);
|
||||
|
||||
const performSearch = (
|
||||
queries: NullAndUndefined<string>[] | undefined,
|
||||
strings: NullAndUndefined<string>[],
|
||||
): boolean => {
|
||||
const actualQueries = queries?.filter((query) => query != null);
|
||||
const actualStrings = strings?.filter((str) => str != null);
|
||||
|
||||
if (!actualQueries?.length) return true;
|
||||
|
||||
const cleanedUpQueries = actualQueries.map(enhancedCleanup);
|
||||
const cleanedUpStrings = actualStrings.map(enhancedCleanup).join(', ');
|
||||
|
||||
return cleanedUpQueries.every((query) => cleanedUpStrings.includes(query));
|
||||
};
|
||||
|
||||
type TMangaQueryFilter = Pick<MangaType, 'title' | 'genre' | 'description' | 'artist' | 'author'> & {
|
||||
source?: NullAndUndefined<Pick<SourceType, 'displayName'>>;
|
||||
};
|
||||
const querySearchManga = (
|
||||
query: NullAndUndefined<string>,
|
||||
{ title, genre: genres, description, artist, author, source }: TMangaQueryFilter,
|
||||
): boolean =>
|
||||
performSearch([query], [title]) ||
|
||||
performSearch(
|
||||
query?.split(','),
|
||||
genres.map((genre) => enhancedCleanup(genre)),
|
||||
) ||
|
||||
performSearch([query], [description]) ||
|
||||
performSearch([query], [artist]) ||
|
||||
performSearch([query], [author]) ||
|
||||
performSearch([query], [source?.displayName]);
|
||||
|
||||
type TMangaTrackerFilter = { trackRecords: { nodes: Pick<TrackRecordType, 'id' | 'trackerId'>[] } };
|
||||
const trackerFilter = (trackFilters: LibraryOptions['hasTrackerBinding'], manga: TMangaTrackerFilter): boolean =>
|
||||
Object.entries(trackFilters)
|
||||
.map(([trackFilterId, trackFilterState]) => {
|
||||
const isTrackerBound = manga.trackRecords.nodes.some(
|
||||
(trackRecord) => trackRecord.trackerId === Number(trackFilterId),
|
||||
);
|
||||
|
||||
return triStateFilter(
|
||||
trackFilterState,
|
||||
() => isTrackerBound,
|
||||
() => !isTrackerBound,
|
||||
);
|
||||
})
|
||||
.every(Boolean);
|
||||
|
||||
type TMangaStatusFilter = Pick<MangaType, 'status'>;
|
||||
const statusFilter = (statusFilters: LibraryOptions['hasStatus'], manga: TMangaStatusFilter): boolean =>
|
||||
Object.entries(statusFilters)
|
||||
.map(([status, statusFilterState]) => triStateFilterBoolean(statusFilterState, status === manga.status))
|
||||
.every(Boolean);
|
||||
|
||||
type TMangaFilterOptions = Pick<
|
||||
LibraryOptions,
|
||||
| 'hasUnreadChapters'
|
||||
| 'hasDownloadedChapters'
|
||||
| 'hasBookmarkedChapters'
|
||||
| 'hasDuplicateChapters'
|
||||
| 'hasTrackerBinding'
|
||||
| 'hasStatus'
|
||||
>;
|
||||
type TMangaFilter = Pick<MangaType, 'downloadCount' | 'unreadCount' | 'bookmarkCount' | 'hasDuplicateChapters'> &
|
||||
TMangaTrackerFilter &
|
||||
TMangaStatusFilter;
|
||||
const filterManga = (
|
||||
manga: TMangaFilter,
|
||||
{
|
||||
hasDownloadedChapters,
|
||||
hasUnreadChapters,
|
||||
hasBookmarkedChapters,
|
||||
hasDuplicateChapters,
|
||||
hasTrackerBinding,
|
||||
hasStatus,
|
||||
}: TMangaFilterOptions,
|
||||
): boolean =>
|
||||
triStateFilterNumber(hasDownloadedChapters, manga.downloadCount) &&
|
||||
triStateFilterNumber(hasUnreadChapters, manga.unreadCount) &&
|
||||
triStateFilterNumber(hasBookmarkedChapters, manga.bookmarkCount) &&
|
||||
triStateFilterBoolean(hasDuplicateChapters, manga.hasDuplicateChapters) &&
|
||||
trackerFilter(hasTrackerBinding, manga) &&
|
||||
statusFilter(hasStatus, manga);
|
||||
|
||||
type TMangasFilter = TMangaQueryFilter & TMangaFilter;
|
||||
const filterMangas = <Manga extends TMangasFilter>(
|
||||
mangas: Manga[],
|
||||
query: NullAndUndefined<string>,
|
||||
options: TMangaFilterOptions & { ignoreFilters: boolean },
|
||||
): Manga[] => {
|
||||
const ignoreFiltersWhileSearching = options.ignoreFilters && query?.length;
|
||||
|
||||
return mangas.filter((manga) => {
|
||||
const matchesSearch = querySearchManga(query, manga);
|
||||
const matchesFilters = ignoreFiltersWhileSearching || filterManga(manga, options);
|
||||
|
||||
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);
|
||||
|
||||
type TMangaSort = Pick<MangaType, 'title' | 'inLibraryAt' | 'unreadCount'> &
|
||||
MangaChapterCountInfo & {
|
||||
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>,
|
||||
): Manga[] => {
|
||||
const result = [...manga];
|
||||
|
||||
switch (sort) {
|
||||
case 'alphabetically':
|
||||
result.sort((a, b) => sortByString(a.title, b.title));
|
||||
break;
|
||||
case 'dateAdded':
|
||||
result.sort((a, b) => sortByNumber(a.inLibraryAt, b.inLibraryAt));
|
||||
break;
|
||||
case 'unreadChapters':
|
||||
result.sort((a, b) => sortByNumber(a.unreadCount, b.unreadCount));
|
||||
break;
|
||||
case 'lastRead':
|
||||
result.sort((a, b) => sortByNumber(a.lastReadChapter?.lastReadAt, b.lastReadChapter?.lastReadAt));
|
||||
break;
|
||||
case 'latestUploadedChapter':
|
||||
result.sort((a, b) =>
|
||||
sortByNumber(a.latestUploadedChapter?.uploadDate, b.latestUploadedChapter?.uploadDate),
|
||||
);
|
||||
break;
|
||||
case 'latestFetchedChapter':
|
||||
result.sort((a, b) => sortByNumber(a.latestFetchedChapter?.fetchedAt, b.latestFetchedChapter?.fetchedAt));
|
||||
break;
|
||||
case 'totalChapters':
|
||||
result.sort((a, b) => sortByNumber(a.chapters.totalCount, b.chapters.totalCount));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (desc) {
|
||||
result.reverse();
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const useGetVisibleLibraryMangas = <Manga extends MangaIdInfo & TMangasFilter & TMangaSort>(
|
||||
mangas: Manga[],
|
||||
category?: CategoryMetadataInfo,
|
||||
): {
|
||||
visibleMangas: Manga[];
|
||||
showFilteredOutMessage: boolean;
|
||||
} => {
|
||||
const [query] = useQueryParam('query', StringParam);
|
||||
const options = getCategoryMetadata(category);
|
||||
const {
|
||||
hasUnreadChapters,
|
||||
hasDownloadedChapters,
|
||||
hasBookmarkedChapters,
|
||||
hasTrackerBinding,
|
||||
hasDuplicateChapters,
|
||||
hasStatus,
|
||||
} = options;
|
||||
const { settings } = useMetadataServerSettings();
|
||||
|
||||
const filteredMangas = useMemo(
|
||||
() =>
|
||||
filterMangas(mangas, query, {
|
||||
...options,
|
||||
ignoreFilters: settings.ignoreFilters,
|
||||
}),
|
||||
[
|
||||
mangas,
|
||||
query,
|
||||
hasUnreadChapters,
|
||||
hasDownloadedChapters,
|
||||
hasBookmarkedChapters,
|
||||
hasTrackerBinding,
|
||||
hasDuplicateChapters,
|
||||
hasStatus,
|
||||
settings.ignoreFilters,
|
||||
],
|
||||
);
|
||||
const sortedMangas = useMemo(
|
||||
() => sortManga(filteredMangas, options.sortBy, options.sortDesc),
|
||||
[filteredMangas, options.sortBy, options.sortDesc],
|
||||
);
|
||||
|
||||
const isATrackFilterActive = Object.values(options.hasTrackerBinding).some(
|
||||
(trackFilterState) => trackFilterState != null,
|
||||
);
|
||||
const showFilteredOutMessage =
|
||||
(hasUnreadChapters != null ||
|
||||
hasDownloadedChapters != null ||
|
||||
hasBookmarkedChapters != null ||
|
||||
!!query ||
|
||||
isATrackFilterActive) &&
|
||||
filteredMangas.length === 0 &&
|
||||
mangas.length > 0;
|
||||
|
||||
return {
|
||||
visibleMangas: sortedMangas,
|
||||
showFilteredOutMessage,
|
||||
};
|
||||
};
|
||||
@@ -15,7 +15,7 @@ import Tooltip from '@mui/material/Tooltip';
|
||||
import React from 'react';
|
||||
import ViewModuleIcon from '@mui/icons-material/ViewModule';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
|
||||
// TODO: clean up this to use a FormControl, and remove dependency on name o radio button
|
||||
export function GridLayouts({
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { GridLayout } from '@/components/context/LibraryOptionsContext';
|
||||
import { GridLayout } from '@/modules/library/contexts/LibraryOptionsContext.tsx';
|
||||
import { GridLayouts } from '@/components/source/GridLayouts.tsx';
|
||||
import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user