diff --git a/public/locales/en.json b/public/locales/en.json index 0ff581bd..345c535e 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1045,6 +1045,9 @@ "source_search_failed": "Could not search source" } }, + "filter": { + "has_results": "Has results" + }, "title": { "global_search": "Global Search", "search": "Search" diff --git a/src/modules/browse/Browse.types.ts b/src/modules/browse/Browse.types.ts index 3c0cea87..b3c591fe 100644 --- a/src/modules/browse/Browse.types.ts +++ b/src/modules/browse/Browse.types.ts @@ -13,4 +13,5 @@ export type MetadataBrowseSettings = { extensionLanguages: string[]; showNsfw: boolean; lastUsedSourceId: SourceIdInfo['id'] | null; + shouldShowOnlySourcesWithResults: boolean; }; diff --git a/src/modules/global-search/screens/SearchAll.tsx b/src/modules/global-search/screens/SearchAll.tsx index 382ae9a0..72129614 100644 --- a/src/modules/global-search/screens/SearchAll.tsx +++ b/src/modules/global-search/screens/SearchAll.tsx @@ -14,8 +14,14 @@ import { Link, useLocation } from 'react-router-dom'; import { StringParam, useQueryParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Button from '@mui/material/Button'; +import PushPinIcon from '@mui/icons-material/PushPin'; +import DoneAllIcon from '@mui/icons-material/DoneAll'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import { useElementSize } from '@mantine/hooks'; import { requestManager } from '@/lib/requests/RequestManager.ts'; -import { useLocalStorage } from '@/modules/core/hooks/useStorage.tsx'; +import { useLocalStorage, useSessionStorage } from '@/modules/core/hooks/useStorage.tsx'; import { getDefaultLanguages } from '@/modules/core/utils/Languages.ts'; import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx'; import { LanguageSelect } from '@/modules/core/components/inputs/LanguageSelect.tsx'; @@ -32,9 +38,13 @@ import { AppRoutes } from '@/modules/core/AppRoute.constants.ts'; import { getErrorMessage } from '@/lib/HelperFunctions.ts'; import { Sources } from '@/modules/source/services/Sources.ts'; import { SourceDisplayNameInfo, SourceIdInfo, SourceMetaInfo } from '@/modules/source/Source.types.ts'; -import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts'; +import { + createUpdateMetadataServerSettings, + useMetadataServerSettings, +} from '@/modules/settings/services/ServerSettingsMetadata.ts'; import { useAppTitleAndAction } from '@/modules/navigation-bar/hooks/useAppTitleAndAction.ts'; import { getSourceMetadata } from '@/modules/source/services/SourceMetadata.ts'; +import { makeToast } from '@/modules/core/utils/Toast.ts'; type SourceLoadingState = { isLoading: boolean; hasResults: boolean; emptySearch: boolean; error: any }; type SourceToLoadingStateMap = Map; @@ -200,31 +210,56 @@ const SourceSearchPreview = React.memo( export const SearchAll: React.FC = () => { const { t } = useTranslation(); - const { pathname, state } = useLocation<{ mangaTitle?: string }>(); + const { ref: filterHeaderRef, height: filterHeaderHeight } = useElementSize(); + const isMigrateMode = pathname.startsWith('/migrate/source'); const [query] = useQueryParam('query', StringParam); const searchString = useDebounce(query, TRIGGER_SEARCH_THRESHOLD); const [shownLangs, setShownLangs] = useLocalStorage('shownSourceLangs', getDefaultLanguages()); + const [shouldShowOnlyPinnedSources, setShouldShowOnlyPinnedSources] = useSessionStorage( + 'SearchAll::shouldShowOnlyPinnedSources', + true, + ); const { - settings: { showNsfw }, + settings: { showNsfw, shouldShowOnlySourcesWithResults }, } = useMetadataServerSettings(); const { data, loading, error, refetch } = requestManager.useGetSourceList({ notifyOnNetworkStatusChange: true }); const sources = useMemo(() => data?.sources.nodes ?? [], [data?.sources.nodes]); - const filteredSources = useMemo( - () => Sources.filter(sources, { showNsfw, languages: shownLangs, keepLocalSource: true }), - [sources, shownLangs], - ); - const sourcesSortedByName = useMemo(() => [...filteredSources].toSorted(compareSourceByName), [filteredSources]); const [sourceToLoadingStateMap, setSourceToLoadingStateMap] = useState(new Map()); const debouncedSourceToLoadingStateMap = useDebounce(sourceToLoadingStateMap, 500); const sourceLanguages = useMemo(() => Sources.getLanguages(sources), [sources]); + const filteredSources = useMemo( + () => + Sources.filter(sources, { + showNsfw, + languages: shownLangs, + keepLocalSource: true, + pinned: shouldShowOnlyPinnedSources, + }), + [sources, shownLangs, shouldShowOnlyPinnedSources], + ); + const filteredSourcesByHasResult = useMemo(() => { + if (!shouldShowOnlySourcesWithResults) { + return filteredSources; + } + + return filteredSources.filter((source) => { + const sourceState = debouncedSourceToLoadingStateMap.get(source.id); + + return !sourceState || sourceState?.isLoading || (sourceState?.hasResults && !sourceState?.error); + }); + }, [filteredSources, shouldShowOnlySourcesWithResults, debouncedSourceToLoadingStateMap]); + const sourcesSortedByName = useMemo( + () => [...filteredSourcesByHasResult].toSorted(compareSourceByName), + [filteredSourcesByHasResult], + ); const sourcesSortedByResult = useMemo( () => [...sourcesSortedByName].sort((sourceA, sourceB) => @@ -244,6 +279,10 @@ export const SearchAll: React.FC = () => { [setSourceToLoadingStateMap], ); + const updateMetadataSettings = createUpdateMetadataServerSettings<'shouldShowOnlySourcesWithResults'>((e) => + makeToast(t('global.error.label.failed_to_save_changes'), 'error', getErrorMessage(e)), + ); + useAppTitleAndAction( t(isMigrateMode ? 'migrate.search.title' : 'search.title.global_search', { title: state?.mangaTitle }), <> @@ -272,18 +311,59 @@ export const SearchAll: React.FC = () => { } return ( - - {sourcesSortedByResult.map((source, index) => ( - - - - ))} + + theme.palette.background.default, + }} + > + + + + + + + + {sourcesSortedByResult.map((source, index) => ( + + + + ))} + ); }; diff --git a/src/modules/metadata/Metadata.constants.ts b/src/modules/metadata/Metadata.constants.ts index 6ae06960..c11b3da8 100644 --- a/src/modules/metadata/Metadata.constants.ts +++ b/src/modules/metadata/Metadata.constants.ts @@ -97,6 +97,7 @@ export const APP_METADATA_KEY_TO_TYPE = { mangaGridItemWidth: 'number', isPinned: 'boolean', lastUsedSourceId: 'string', + shouldShowOnlySourcesWithResults: 'boolean', } as const satisfies Record; export const VALID_APP_METADATA_KEYS = Object.keys(APP_METADATA_KEY_TO_TYPE); @@ -149,6 +150,7 @@ export const GLOBAL_METADATA_KEYS: AppMetadataKeys[] = [ 'extensionLanguages', 'showNsfw', 'lastUsedSourceId', + 'shouldShowOnlySourcesWithResults', // history 'hideHistory', diff --git a/src/modules/settings/Settings.constants.ts b/src/modules/settings/Settings.constants.ts index 7f9088bc..ed61dbc0 100644 --- a/src/modules/settings/Settings.constants.ts +++ b/src/modules/settings/Settings.constants.ts @@ -45,6 +45,7 @@ export const SERVER_SETTINGS_METADATA_DEFAULT: MetadataServerSettings = { extensionLanguages: getDefaultLanguages(), showNsfw: true, lastUsedSourceId: null, + shouldShowOnlySourcesWithResults: true, // history hideHistory: false, diff --git a/src/modules/source/services/Sources.ts b/src/modules/source/services/Sources.ts index a7ec2316..832b2e40 100644 --- a/src/modules/source/services/Sources.ts +++ b/src/modules/source/services/Sources.ts @@ -80,7 +80,8 @@ export class Sources { showNsfw, languages, keepLocalSource, - }: { showNsfw?: boolean; languages?: string[]; keepLocalSource?: boolean } = {}, + pinned, + }: { showNsfw?: boolean; languages?: string[]; keepLocalSource?: boolean; pinned?: boolean } = {}, ): Source[] { const normalizedLanguages = toComparableLanguages(toUniqueLanguageCodes(languages ?? [])); @@ -97,6 +98,13 @@ export class Sources { !languages || normalizedLanguages.includes(toComparableLanguage(Sources.getLanguage(source))) || (keepLocalSource && Sources.isLocalSource(source)), + ) + .filter( + (source) => + pinned === undefined || + !pinned || + getSourceMetadata(source).isPinned || + (keepLocalSource && Sources.isLocalSource(source)), ); }