diff --git a/CHANGELOG.md b/CHANGELOG.md index 87449782..f9363671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added +- (**Library**) Add source filter to library filtering options - (**Migration**) Add a search option to ignore outdated matches - (**Migration**) Add a search option to ignore matches with missing chapters - (**Migration**) Add "local source" as a possible destination source diff --git a/src/features/category/services/CategoryMetadata.ts b/src/features/category/services/CategoryMetadata.ts index ec942c04..aea35a34 100644 --- a/src/features/category/services/CategoryMetadata.ts +++ b/src/features/category/services/CategoryMetadata.ts @@ -36,6 +36,7 @@ export const DEFAULT_CATEGORY_METADATA: ICategoryMetadata = { hasDuplicateChapters: undefined, hasTrackerBinding: {}, hasStatus: {} as LibraryOptions['hasStatus'], + hasSource: {}, }; const convertAppMetadataToGqlMetadata = ( @@ -44,6 +45,7 @@ const convertAppMetadataToGqlMetadata = ( ...metadata, hasTrackerBinding: metadata.hasTrackerBinding ? JSON.stringify(metadata.hasTrackerBinding) : undefined, hasStatus: metadata.hasStatus ? JSON.stringify(metadata.hasStatus) : undefined, + hasSource: metadata.hasSource ? JSON.stringify(metadata.hasSource) : undefined, }); const getCategoryMetadataWithDefaultValueFallback = ( diff --git a/src/features/library/Library.types.ts b/src/features/library/Library.types.ts index 9d5dac71..760b871d 100644 --- a/src/features/library/Library.types.ts +++ b/src/features/library/Library.types.ts @@ -43,6 +43,7 @@ export interface LibraryOptions { hasDuplicateChapters: NullAndUndefined; hasTrackerBinding: Record>; hasStatus: Record>; + hasSource: Record>; } export type TMangaDuplicate = MangaIdInfo & MangaTitleInfo & MangaDescriptionInfo; diff --git a/src/features/library/components/LibraryOptionsPanel.tsx b/src/features/library/components/LibraryOptionsPanel.tsx index 0963f1f6..2480d90c 100644 --- a/src/features/library/components/LibraryOptionsPanel.tsx +++ b/src/features/library/components/LibraryOptionsPanel.tsx @@ -11,6 +11,8 @@ import FormLabel from '@mui/material/FormLabel'; import RadioGroup from '@mui/material/RadioGroup'; import { useLingui } from '@lingui/react/macro'; import { msg } from '@lingui/core/macro'; +import { useMemo } from 'react'; +import uniqBy from 'lodash/fp/uniqBy'; import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx'; import { RadioInput } from '@/base/components/inputs/RadioInput.tsx'; import { SortRadioInput } from '@/base/components/inputs/SortRadioInput.tsx'; @@ -65,6 +67,16 @@ export const LibraryOptionsPanel = ({ const trackerList = requestManager.useGetTrackerList(GET_TRACKERS_SETTINGS); const loggedInTrackers = Trackers.getLoggedIn(trackerList.data?.trackers.nodes ?? STABLE_EMPTY_ARRAY); + const migratableSourcesResult = requestManager.useGetMigratableSources(); + const librarySources = useMemo(() => { + const sources = migratableSourcesResult.data?.mangas.nodes + .map(({ source }) => source) + .filter((source) => source != null); + const uniqueSources = uniqBy('id', sources); + + return uniqueSources.toSorted((a, b) => a.displayName.localeCompare(b.displayName)); + }, [migratableSourcesResult.data?.mangas.nodes]); + const categoryLibraryOptions = useGetCategoryMetadata(category); const updateCategoryLibraryOptions = createUpdateCategoryMetadata(category, (e) => makeToast(t`Failed to save changes`, 'error', getErrorMessage(e)), @@ -140,6 +152,24 @@ export const LibraryOptionsPanel = ({ } /> ))} + {librarySources.length > 0 && ( + <> + {t`Source`} + {librarySources.map((source) => ( + + updateCategoryLibraryOptions('hasSource', { + ...categoryLibraryOptions.hasSource, + [source.id]: checked, + }) + } + /> + ))} + + )} ); } diff --git a/src/features/library/components/LibraryToolbarMenu.tsx b/src/features/library/components/LibraryToolbarMenu.tsx index 34baef0b..d17aa5d6 100644 --- a/src/features/library/components/LibraryToolbarMenu.tsx +++ b/src/features/library/components/LibraryToolbarMenu.tsx @@ -31,7 +31,8 @@ export const LibraryToolbarMenu = ({ options.hasBookmarkedChapters != null || options.hasDuplicateChapters != null || Object.values(options.hasStatus).some((hasStatus) => hasStatus != null) || - Object.values(options.hasTrackerBinding).some((trackerFilterStatus) => trackerFilterStatus != null); + Object.values(options.hasTrackerBinding).some((trackerFilterStatus) => trackerFilterStatus != null) || + Object.values(options.hasSource).some((sourceFilterStatus) => sourceFilterStatus != null); return ( <> diff --git a/src/features/library/hooks/useGetVisibleLibraryMangas.ts b/src/features/library/hooks/useGetVisibleLibraryMangas.ts index d7501413..be791265 100644 --- a/src/features/library/hooks/useGetVisibleLibraryMangas.ts +++ b/src/features/library/hooks/useGetVisibleLibraryMangas.ts @@ -117,6 +117,11 @@ const statusFilter = (statusFilters: LibraryOptions['hasStatus'], manga: MangaSt .map(([status, statusFilterState]) => triStateFilterBoolean(statusFilterState, status === manga.status)) .every(Boolean); +const sourceFilter = (sourceFilters: LibraryOptions['hasSource'], manga: MangaSourceIdInfo): boolean => + Object.entries(sourceFilters) + .map(([sourceId, sourceFilterState]) => triStateFilterBoolean(sourceFilterState, sourceId === manga.sourceId)) + .every(Boolean); + type TMangaFilterOptions = Pick< LibraryOptions, | 'hasUnreadChapters' @@ -126,10 +131,12 @@ type TMangaFilterOptions = Pick< | 'hasDuplicateChapters' | 'hasTrackerBinding' | 'hasStatus' + | 'hasSource' >; type TMangaFilter = Pick & TMangaTrackerFilter & MangaStatusInfo & + MangaSourceIdInfo & MangaChapterCountInfo & MangaDownloadInfo & MangaUnreadInfo; @@ -143,6 +150,7 @@ const filterManga = ( hasDuplicateChapters, hasTrackerBinding, hasStatus, + hasSource, }: TMangaFilterOptions, ): boolean => triStateFilterNumber(hasDownloadedChapters, manga.downloadCount) && @@ -151,7 +159,8 @@ const filterManga = ( triStateFilterNumber(hasBookmarkedChapters, manga.bookmarkCount) && triStateFilterBoolean(hasDuplicateChapters, manga.hasDuplicateChapters) && trackerFilter(hasTrackerBinding, manga) && - statusFilter(hasStatus, manga); + statusFilter(hasStatus, manga) && + sourceFilter(hasSource, manga); type TMangasFilter = TMangaQueryFilter & TMangaFilter; const filterMangas = ( @@ -243,6 +252,7 @@ export const useGetVisibleLibraryMangas = trackFilterState != null, ); + const isASourceFilterActive = Object.values(options.hasSource).some( + (sourceFilterState) => sourceFilterState != null, + ); const showFilteredOutMessage = (hasUnreadChapters != null || hasReadChapters != null || hasDownloadedChapters != null || hasBookmarkedChapters != null || !!query || - isATrackFilterActive) && + isATrackFilterActive || + isASourceFilterActive) && filteredMangas.length === 0 && mangas.length > 0; diff --git a/src/features/metadata/Metadata.constants.ts b/src/features/metadata/Metadata.constants.ts index 0e3a0a17..e3516a27 100644 --- a/src/features/metadata/Metadata.constants.ts +++ b/src/features/metadata/Metadata.constants.ts @@ -200,6 +200,9 @@ export const APP_METADATA: Record< hasStatus: { convert: convertToObject, }, + hasSource: { + convert: convertToObject, + }, customThemes: { convert: convertToObject, }, @@ -440,6 +443,7 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [ 'hasDuplicateChapters', 'hasTrackerBinding', 'hasStatus', + 'hasSource', // sort 'sortBy', 'sortDesc', diff --git a/src/i18n/locales/en.po b/src/i18n/locales/en.po index c3655b04..600708f1 100644 --- a/src/i18n/locales/en.po +++ b/src/i18n/locales/en.po @@ -3504,6 +3504,7 @@ msgstr "Sort" #: src/features/browse/screens/Browse.tsx #: src/features/browse/screens/BrowseSettings.tsx +#: src/features/library/components/LibraryOptionsPanel.tsx #: src/features/manga/components/details/MangaDetails.tsx #: src/features/source/browse/screens/SourceMangas.tsx msgid "Source"