2021-03-26 04:17:02 +04:30
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
2021-01-26 23:32:12 +03:30
* License, v. 2.0. If a copy of the MPL was not distributed with this
2023-05-18 13:17:41 +02:00
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
2021-01-26 23:32:12 +03:30
2026-01-10 19:45:02 +01:00
import { MessageDescriptor } from '@lingui/core' ;
2025-05-03 13:24:55 +02:00
import { useCallback , useEffect , useMemo , useRef , useState } from 'react' ;
2024-12-27 04:11:02 +01:00
import { useParams , useNavigate , useLocation , useSearchParams } from 'react-router-dom' ;
2021-09-09 22:57:19 +04:30
import IconButton from '@mui/material/IconButton' ;
import SettingsIcon from '@mui/icons-material/Settings' ;
2022-03-04 01:25:10 +00:00
import { useQueryParam , StringParam } from 'use-query-params' ;
2023-05-12 11:52:00 +02:00
import Link from '@mui/material/Link' ;
2024-04-14 15:50:12 +02:00
import Box from '@mui/material/Box' ;
import Button from '@mui/material/Button' ;
import { styled } from '@mui/material/styles' ;
2023-05-28 12:16:06 +02:00
import FavoriteIcon from '@mui/icons-material/Favorite' ;
import NewReleasesIcon from '@mui/icons-material/NewReleases' ;
import FilterListIcon from '@mui/icons-material/FilterList' ;
2026-01-10 19:45:02 +01:00
import { useLingui } from '@lingui/react/macro' ;
import { msg } from '@lingui/core/macro' ;
2025-08-16 02:48:46 +02:00
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx' ;
2023-11-11 23:43:22 +01:00
import {
requestManager ,
AbortableApolloUseMutationPaginatedResponse ,
SPECIAL_ED_SOURCES ,
2024-10-06 01:27:51 +02:00
} from '@/lib/requests/RequestManager.ts' ;
2025-08-15 22:02:58 +02:00
import { SourceGridLayout } from '@/features/source/components/SourceGridLayout.tsx' ;
2025-08-16 02:48:46 +02:00
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx' ;
2025-08-16 15:48:07 +02:00
import { SourceOptions } from '@/features/source/browse/components/SourceOptions.tsx' ;
2025-08-15 22:02:58 +02:00
import { BaseMangaGrid } from '@/features/manga/components/BaseMangaGrid.tsx' ;
2023-09-12 02:36:11 +02:00
import {
2024-07-03 23:15:50 +02:00
GetSourceBrowseQuery ,
GetSourceBrowseQueryVariables ,
2023-09-12 02:36:11 +02:00
GetSourceMangasFetchMutation ,
GetSourceMangasFetchMutationVariables ,
} from '@/lib/graphql/generated/graphql.ts' ;
2025-05-17 23:37:00 +02:00
import {
updateMetadataServerSettings ,
useMetadataServerSettings ,
2025-08-15 22:02:58 +02:00
} from '@/features/settings/services/ServerSettingsMetadata.ts' ;
2025-08-16 02:48:46 +02:00
import { useLocalStorage , useSessionStorage } from '@/base/hooks/useStorage.tsx' ;
2025-08-15 22:02:58 +02:00
import { MANGA_GRID_SNAPSHOT_KEY } from '@/features/manga/components/MangaGrid.tsx' ;
import { createUpdateSourceMetadata , useGetSourceMetadata } from '@/features/source/services/SourceMetadata.ts' ;
2025-08-16 02:48:46 +02:00
import { makeToast } from '@/base/utils/Toast.ts' ;
2025-12-22 14:00:44 +01:00
import { GET_SOURCE_BROWSE } from '@/lib/graphql/source/SourceQuery.ts' ;
2025-08-15 22:02:58 +02:00
import { IPos , SourceIdInfo } from '@/features/source/Source.types.ts' ;
2024-10-06 16:15:01 +02:00
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' ;
2025-08-16 02:48:46 +02:00
import { EmptyView } from '@/base/components/feedback/EmptyView.tsx' ;
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx' ;
2025-08-15 22:02:58 +02:00
import { MangaIdInfo } from '@/features/manga/Manga.types.ts' ;
2026-01-10 19:45:02 +01:00
import { GridLayout , SearchParam } from '@/base/Base.types' ;
2025-08-16 02:48:46 +02:00
import { AppRoutes } from '@/base/AppRoute.constants.ts' ;
2024-12-20 17:24:40 +01:00
import { getErrorMessage } from '@/lib/HelperFunctions.ts' ;
2025-08-15 22:02:58 +02:00
import { Sources } from '@/features/source/services/Sources.ts' ;
import { useAppTitleAndAction } from '@/features/navigation-bar/hooks/useAppTitleAndAction.ts' ;
2025-08-16 02:14:49 +02:00
import { useNavBarContext } from '@/features/navigation-bar/NavbarContext.tsx' ;
2025-05-25 15:36:25 +02:00
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.tsx' ;
2025-09-08 00:58:12 +02:00
import { IconWebView } from '@/assets/icons/IconWebView.tsx' ;
2024-10-26 17:25:04 +02:00
2025-04-21 17:00:13 +02:00
const DEFAULT_SOURCE : SourceIdInfo = { id : '-1' };
2023-05-28 12:16:06 +02:00
const ContentTypeMenu = styled ( 'div' )(({ theme }) => ({
display : 'flex' ,
2024-09-14 03:01:07 +02:00
position : 'sticky' ,
2023-05-28 12:16:06 +02:00
width : '100%' ,
zIndex : 1 ,
2024-09-14 03:01:07 +02:00
padding : theme.spacing ( 1 ),
gap : theme.spacing ( 1 ),
2023-05-28 12:16:06 +02:00
backgroundColor : theme.palette.background.default ,
}));
2024-09-14 03:01:07 +02:00
const ContentTypeButton = styled ( Button )(() => ({}));
2023-05-28 12:16:06 +02:00
2024-09-14 03:01:07 +02:00
const StyledGridWrapper = styled ( Box )(() => ({
minHeight : '100%' ,
position : 'relative' ,
}));
2023-05-28 12:16:06 +02:00
export enum SourceContentType {
POPULAR ,
LATEST ,
SEARCH ,
}
2022-03-04 01:25:10 +00:00
2026-01-10 19:45:02 +01:00
const SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY : { [ contentType in SourceContentType ] : MessageDescriptor } = {
[ SourceContentType . POPULAR ] : msg `No manga found` ,
[ SourceContentType . LATEST ] : msg `No manga found` ,
[ SourceContentType . SEARCH ] : msg `No manga matches this filter` ,
2023-05-28 12:16:06 +02:00
};
2024-07-08 18:02:50 +02:00
const getUniqueMangas = < Manga extends MangaIdInfo >( mangas : Manga []) : Manga [] => {
const mangaIdToManga : Record < string , Manga > = {};
const uniqueMangas : Manga [] = [];
2023-05-28 12:16:06 +02:00
mangas . forEach (( manga ) => {
2024-05-05 23:27:52 +02:00
const isDuplicate = !! mangaIdToManga [ manga . id ];
2023-05-28 12:16:06 +02:00
if ( ! isDuplicate ) {
2024-05-05 23:27:52 +02:00
mangaIdToManga [ manga . id ] = manga ;
2023-05-28 12:16:06 +02:00
uniqueMangas . push ( manga );
}
});
return uniqueMangas ;
};
const useSourceManga = (
sourceId : string ,
contentType : SourceContentType ,
searchTerm : string | null | undefined ,
filters : IPos [],
2023-09-12 02:36:11 +02:00
initialPages : number ,
2024-03-23 23:44:46 +01:00
hideLibraryEntries : boolean ,
2023-09-12 02:36:11 +02:00
) : [
AbortableApolloUseMutationPaginatedResponse < GetSourceMangasFetchMutation , GetSourceMangasFetchMutationVariables >[ 0 ],
AbortableApolloUseMutationPaginatedResponse <
GetSourceMangasFetchMutation ,
GetSourceMangasFetchMutationVariables
2024-03-23 23:44:46 +01:00
> [ 1 ][ number ] & { filteredOutAllItemsOfFetchedPage : boolean },
2023-09-12 02:36:11 +02:00
] => {
let result : AbortableApolloUseMutationPaginatedResponse <
GetSourceMangasFetchMutation ,
GetSourceMangasFetchMutationVariables
> ;
2023-05-28 12:16:06 +02:00
switch ( contentType ) {
case SourceContentType.POPULAR :
2023-06-17 16:50:17 +02:00
result = requestManager . useGetSourcePopularMangas ( sourceId , initialPages );
2023-05-28 12:16:06 +02:00
break ;
case SourceContentType.LATEST :
2023-06-17 16:50:17 +02:00
result = requestManager . useGetSourceLatestMangas ( sourceId , initialPages );
2023-05-28 12:16:06 +02:00
break ;
case SourceContentType.SEARCH :
2023-09-24 21:32:23 +02:00
result = requestManager . useSourceSearch (
sourceId ,
2024-01-11 02:57:56 +01:00
searchTerm ?? '' ,
2023-09-24 21:32:23 +02:00
filters . map (( filter ) => {
const { position , state , group } = filter ;
const isPartOfGroup = group !== undefined ;
if ( isPartOfGroup ) {
return {
position : group ,
groupChange : {
position ,
[ filter . type ] : state ,
},
};
}
return {
position ,
[ filter . type ] : state ,
};
}),
initialPages ,
);
2023-05-28 12:16:06 +02:00
break ;
default :
throw new Error ( `Unknown ContentType " ${ contentType } "` );
}
2023-09-12 02:36:11 +02:00
const pages = result [ 1 ] ! ;
const lastLoadedPageIndex = pages . findLastIndex (( page ) => !! page . data ? . fetchSourceManga );
const lastLoadedPage = pages [ lastLoadedPageIndex ];
2023-05-28 12:16:06 +02:00
2024-03-23 23:44:46 +01:00
const isPageLoading = pages . slice ( - 1 )[ 0 ]. isLoading ;
let filteredOutAllItemsOfFetchedPage = ! isPageLoading ;
const items = useMemo (() => {
2024-06-02 15:24:29 +02:00
type FetchItemsResult = NonNullable < GetSourceMangasFetchMutation [ 'fetchSourceManga' ] >[ 'mangas' ];
2024-03-23 23:44:46 +01:00
let allItems : FetchItemsResult = [];
pages . forEach (( page , index ) => {
2024-07-11 17:55:00 +02:00
const pageItems = page . data ? . fetchSourceManga ? . mangas ?? [];
2024-05-05 23:27:52 +02:00
const nonLibraryPageItems = pageItems . filter (( item ) => ! hideLibraryEntries || ! item . inLibrary );
const uniqueItems = getUniqueMangas ([... allItems , ... nonLibraryPageItems ]);
2024-03-23 23:44:46 +01:00
const isLastPage = ! isPageLoading && pages . length === index + 1 ;
2024-05-05 23:27:52 +02:00
filteredOutAllItemsOfFetchedPage = isLastPage && ! nonLibraryPageItems . length && !! pageItems . length ;
allItems = uniqueItems ;
2024-03-23 23:44:46 +01:00
});
return allItems ;
}, [ pages , hideLibraryEntries ]);
if ( lastLoadedPageIndex === - 1 ) {
return [ result [ 0 ], { ... result [ 1 ][ result [ 1 ]. length - 1 ], filteredOutAllItemsOfFetchedPage }];
2023-09-12 02:36:11 +02:00
}
return [
result [ 0 ],
{
... pages [ pages . length - 1 ],
data : {
... lastLoadedPage ! . data ,
fetchSourceManga : {
... lastLoadedPage ! . data ! . fetchSourceManga ,
hasNextPage :
pages.length > lastLoadedPageIndex + 1
? false
2024-06-02 15:24:29 +02:00
: !! lastLoadedPage ! . data ! . fetchSourceManga ? . hasNextPage ,
2024-03-23 23:44:46 +01:00
mangas : items ,
2023-09-12 02:36:11 +02:00
},
},
2024-03-23 23:44:46 +01:00
filteredOutAllItemsOfFetchedPage ,
2023-09-12 02:36:11 +02:00
},
];
2023-05-28 12:16:06 +02:00
};
2023-10-28 00:32:02 +02:00
export function SourceMangas() {
2026-01-10 19:45:02 +01:00
const { t } = useLingui ();
2025-05-03 13:24:55 +02:00
const { appBarHeight } = useNavBarContext ();
2023-11-12 03:21:26 +01:00
2021-05-13 17:46:40 +04:30
const { sourceId } = useParams < { sourceId : string } > ();
2023-05-18 13:25:19 +02:00
2024-12-27 04:11:02 +01:00
const [ searchParams , setSearchParams ] = useSearchParams ();
2023-06-04 21:30:15 +02:00
const navigate = useNavigate ();
2024-04-16 02:49:08 +02:00
const location = useLocation ();
const { key : locationKey , state : locationState } = location ;
2024-04-07 15:22:03 +02:00
const { contentType : initialContentType = SourceContentType . POPULAR , clearCache = false } =
useLocation < {
contentType : SourceContentType ;
clearCache : boolean ;
} > (). state ?? {};
2023-11-12 03:21:26 +01:00
2024-03-23 23:44:46 +01:00
const {
settings : { hideLibraryEntries },
} = useMetadataServerSettings ();
2024-12-27 04:53:25 +01:00
2024-09-17 03:40:55 +02:00
const [ sourceGridLayout ] = useLocalStorage ( 'source-grid-layout' , GridLayout . Compact );
2025-07-23 01:52:41 +02:00
const [ query ] = useQueryParam ( SearchParam . QUERY , StringParam );
2024-04-14 15:18:51 +02:00
const [ currentFiltersToApply , setCurrentFiltersToApply ] = useSessionStorage < IPos [] | undefined >(
`source-mangas- ${ sourceId } -filters` ,
2024-04-07 15:22:03 +02:00
[],
);
2024-04-14 15:18:51 +02:00
const [ filtersToApply , setLocationFiltersToApply ] = useSessionStorage < IPos [] >(
`source-mangas-location- ${ locationKey } - ${ sourceId } -filters` ,
currentFiltersToApply ?? [],
);
2024-04-07 15:22:03 +02:00
const [ dialogFiltersToApply , setDialogFiltersToApply ] = useState < IPos [] >( filtersToApply );
2024-04-14 15:22:35 +02:00
const [ currentContentType , setCurrentContentType ] = useSessionStorage < SourceContentType | undefined >(
`source-mangas- ${ sourceId } -content-type` ,
initialContentType ,
);
const [ contentType , setLocationContentType ] = useSessionStorage (
2024-04-07 15:22:03 +02:00
`source-mangas-location- ${ locationKey } - ${ sourceId } -content-type` ,
2024-04-14 15:22:35 +02:00
query ? SourceContentType.SEARCH : currentContentType ! ,
2024-04-07 15:22:03 +02:00
);
2025-05-25 15:36:25 +02:00
const { key : persistedGridStateKey , deleteState : deletePersistedGridState } =
VirtuosoUtil . usePersistState ( MANGA_GRID_SNAPSHOT_KEY );
2024-10-07 13:07:57 +02:00
const scrollToTop = useCallback (() => {
2025-05-25 15:36:25 +02:00
deletePersistedGridState ();
2024-10-07 13:07:57 +02:00
window . scrollTo ( 0 , 0 );
2025-05-25 15:36:25 +02:00
}, [ persistedGridStateKey ]);
2024-10-07 13:07:57 +02:00
const currentQuery = useRef ( query );
const currentAbortRequest = useRef < ( reason : any ) => void > (() => {});
const didSearchChange = currentQuery . current !== query ;
if ( didSearchChange && contentType === SourceContentType . SEARCH ) {
currentQuery . current = query ;
currentAbortRequest . current ( new Error ( `SourceMangas( ${ sourceId } ): search string changed` ));
scrollToTop ();
}
2024-04-14 15:18:51 +02:00
useEffect (
() => () => {
setCurrentFiltersToApply ( undefined );
2024-04-14 15:22:35 +02:00
setCurrentContentType ( undefined );
2024-04-14 15:18:51 +02:00
},
[ sourceId ],
);
const setFiltersToApply = ( filters : IPos []) => {
setCurrentFiltersToApply ( filters );
setLocationFiltersToApply ( filters );
2024-04-16 02:49:08 +02:00
scrollToTop ();
2024-04-14 15:18:51 +02:00
};
2024-04-14 15:22:35 +02:00
const setContentType = ( newContentType : SourceContentType ) => {
setCurrentContentType ( newContentType );
setLocationContentType ( newContentType );
};
2025-05-17 19:53:56 +02:00
const gridKey = ` ${ contentType }${ JSON . stringify ( filtersToApply ) }${ query } ` ;
2024-10-06 16:15:01 +02:00
const [
loadPage ,
{ data , error , isLoading : loading , size : lastPageNum , abortRequest , filteredOutAllItemsOfFetchedPage },
] = useSourceManga ( sourceId , contentType , query , filtersToApply , 1 , hideLibraryEntries );
2024-10-07 13:07:57 +02:00
currentAbortRequest . current = abortRequest ;
2024-06-02 15:24:29 +02:00
const mangas = data ? . fetchSourceManga ? . mangas ?? [];
const hasNextPage = !! data ? . fetchSourceManga ? . hasNextPage ;
2025-08-18 22:43:40 +02:00
const isLoading = loading || ( filteredOutAllItemsOfFetchedPage && hasNextPage );
2024-07-03 23:15:50 +02:00
const { data : sourceData } = requestManager . useGetSource < GetSourceBrowseQuery , GetSourceBrowseQueryVariables >(
GET_SOURCE_BROWSE ,
sourceId ,
);
2023-09-12 02:36:11 +02:00
const source = sourceData ? . source ;
2024-05-08 04:01:23 +02:00
2023-09-24 21:32:23 +02:00
const filters = source ? . filters ?? [];
2024-10-26 17:25:04 +02:00
const { savedSearches = {} } = useGetSourceMetadata ( source ?? DEFAULT_SOURCE );
2024-12-20 17:24:40 +01:00
const updateSourceMetadata = createUpdateSourceMetadata < 'savedSearches' > ( source ?? { id : '-1' }, ( e ) =>
2026-01-10 19:45:02 +01:00
makeToast ( t `Failed to save changes` , 'error' , getErrorMessage ( e )),
2024-05-08 04:01:23 +02:00
);
const selectSavedSearch = useCallback (
( savedSearch : string ) => {
const { query : savedSearchQuery , filters : savedSearchFilters } = savedSearches [ savedSearch ];
if ( savedSearchFilters ) {
setDialogFiltersToApply ( savedSearchFilters );
setFiltersToApply ( savedSearchFilters );
}
2024-12-27 04:11:02 +01:00
if ( savedSearchQuery ) {
2025-07-23 01:52:41 +02:00
searchParams . set ( SearchParam . QUERY , savedSearchQuery );
2024-12-27 04:11:02 +01:00
setSearchParams ( searchParams );
}
2024-05-08 04:01:23 +02:00
},
[ savedSearches , locationState ],
);
const handleSavedSearchesUpdate = useCallback (
( savedSearch : string , updateType : 'create' | 'delete' ) => {
if ( updateType === 'delete' ) {
const savedSearchesCopy = { ... savedSearches };
delete savedSearchesCopy [ savedSearch ];
updateSourceMetadata ( 'savedSearches' , savedSearchesCopy );
return ;
}
const updatedSavedSearches = {
... savedSearches ,
2024-12-20 19:27:22 +01:00
[ savedSearch ] : { query : query ?? undefined , filters : dialogFiltersToApply },
2024-05-08 04:01:23 +02:00
};
updateSourceMetadata ( 'savedSearches' , updatedSavedSearches );
},
2024-12-20 19:27:22 +01:00
[ savedSearches , query , dialogFiltersToApply ],
2024-05-08 04:01:23 +02:00
);
2022-03-06 21:02:06 +00:00
2023-07-04 21:53:25 +02:00
const message = ! isLoading ? t ( SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY [ contentType ]) : undefined ;
2025-04-21 17:00:13 +02:00
const isLocalSource = sourceId === Sources . LOCAL_SOURCE_ID ;
2023-05-28 12:16:06 +02:00
const messageExtra = isLocalSource ? (
<>
2026-01-10 19:45:02 +01:00
< span >{ t `Check out` } </ span >
2024-05-04 23:54:33 +02:00
< Link href = "https://github.com/Suwayomi/Suwayomi-Server/wiki/Local-Source" target = "_blank" rel = "noreferrer" >
2026-01-10 19:45:02 +01:00
{ t `Local source guide` }
2023-05-28 12:16:06 +02:00
</ Link >
</>
) : undefined ;
2023-06-17 16:29:55 +02:00
const updateContentType = useCallback (
2024-04-07 15:22:03 +02:00
( newContentType : SourceContentType , newSearch? : string | null ) => {
setContentType ( newContentType );
2024-04-16 02:49:08 +02:00
scrollToTop ();
2024-04-07 15:22:03 +02:00
if ( query && ! newSearch ) {
2024-01-11 02:57:56 +01:00
navigate (
{
2024-04-07 15:22:03 +02:00
pathname : '' ,
},
{
state : { ... locationState , contentType : newContentType },
2024-01-11 02:57:56 +01:00
},
);
2023-06-17 16:29:55 +02:00
}
},
2024-04-16 02:49:08 +02:00
[ setContentType , query , scrollToTop ],
2023-09-24 22:34:37 +02:00
);
2024-04-07 15:22:03 +02:00
const setSearchContentType = !! query && contentType !== SourceContentType . SEARCH ;
2023-05-28 12:16:06 +02:00
if ( setSearchContentType ) {
2024-04-07 15:22:03 +02:00
updateContentType ( SourceContentType . SEARCH , query );
2023-05-28 12:16:06 +02:00
}
2021-09-13 19:18:21 +04:30
2023-06-17 16:50:17 +02:00
const loadMore = useCallback (() => {
if ( ! hasNextPage ) {
2023-05-18 13:25:19 +02:00
return ;
}
2023-09-12 02:36:11 +02:00
loadPage ( lastPageNum + 1 );
}, [ lastPageNum , hasNextPage , contentType ]);
2021-01-22 17:00:33 +03:30
2024-05-08 04:01:23 +02:00
const resetFilters = () => {
2023-05-28 12:16:06 +02:00
setDialogFiltersToApply ([]);
setFiltersToApply ([]);
2024-05-08 04:01:23 +02:00
};
2023-05-28 12:16:06 +02:00
2025-05-17 23:37:00 +02:00
useEffect (() => {
updateMetadataServerSettings ( 'lastUsedSourceId' , sourceId ). catch (
defaultPromiseErrorHandler ( 'SourceMangas::setLastUsedSourceId' ),
);
}, [ sourceId ]);
2024-03-23 23:44:46 +01:00
useEffect (() => {
if ( filteredOutAllItemsOfFetchedPage && hasNextPage && ! loading ) {
loadPage ( lastPageNum + 1 );
}
}, [ filteredOutAllItemsOfFetchedPage , loading ]);
2023-11-11 23:43:22 +01:00
useEffect (() => {
if ( ! clearCache ) {
return ;
}
2025-12-14 01:01:55 +01:00
const requiresClear = SPECIAL_ED_SOURCES . REVALIDATION_UNSUPPORTED . includes ( sourceId );
2023-11-11 23:43:22 +01:00
if ( ! requiresClear ) {
return ;
}
requestManager . clearBrowseCacheFor ( sourceId );
}, [ clearCache ]);
2025-05-03 13:37:20 +02:00
useAppTitleAndAction (
2026-01-10 19:45:02 +01:00
source ? . displayName ?? t `Source` ,
2025-05-03 13:24:55 +02:00
<>
< AppbarSearch />
< SourceGridLayout />
2026-01-10 19:45:02 +01:00
< CustomTooltip title = { t `Open in WebView` } disabled = { ! source ? . baseUrl }>
2025-09-08 00:58:12 +02:00
< IconButton
disabled = { ! source ? . baseUrl }
href = { source ? . baseUrl ? requestManager . getWebviewUrl ( source ? . baseUrl ) : '' }
rel = "noreferrer"
target = "_blank"
color = "inherit"
>
< IconWebView />
</ IconButton >
</ CustomTooltip >
2025-05-03 13:24:55 +02:00
{ source ? . isConfigurable && (
2026-01-10 19:45:02 +01:00
< CustomTooltip title = { t `Settings` }>
2025-05-03 13:24:55 +02:00
< IconButton
onClick = {() => navigate ( AppRoutes . sources . childRoutes . configure . path ( sourceId ))}
aria-label = "display more actions"
edge = "end"
color = "inherit"
>
< SettingsIcon />
</ IconButton >
</ CustomTooltip >
)}
</>,
2025-05-03 13:37:20 +02:00
[ source ],
2025-05-03 13:24:55 +02:00
);
2021-09-18 00:05:00 +04:30
2024-12-20 19:15:18 +01:00
const EmptyViewComponent = mangas . length ? EmptyView : EmptyViewAbsoluteCentered ;
2021-01-22 21:11:00 +03:30
return (
2024-09-14 03:01:07 +02:00
< StyledGridWrapper >
2024-12-21 23:21:55 +01:00
< ContentTypeMenu sx = {{ top : ` ${ appBarHeight } px` }}>
2023-05-28 12:16:06 +02:00
< ContentTypeButton
variant = { contentType === SourceContentType . POPULAR ? 'contained' : 'outlined' }
startIcon = {< FavoriteIcon />}
onClick = {() => updateContentType ( SourceContentType . POPULAR )}
>
2026-01-10 19:45:02 +01:00
{ t `Popular` }
2023-05-28 12:16:06 +02:00
</ ContentTypeButton >
2023-11-05 17:22:16 +01:00
{ source ? . supportsLatest === undefined || source . supportsLatest ? (
< ContentTypeButton
disabled = { ! source ? . supportsLatest }
variant = { contentType === SourceContentType . LATEST ? 'contained' : 'outlined' }
startIcon = {< NewReleasesIcon />}
onClick = {() => updateContentType ( SourceContentType . LATEST )}
>
2026-01-10 19:45:02 +01:00
{ t `Latest` }
2023-11-05 17:22:16 +01:00
</ ContentTypeButton >
) : null }
2023-05-28 12:16:06 +02:00
< ContentTypeButton
2024-01-11 02:57:56 +01:00
variant = { contentType === SourceContentType . SEARCH ? 'contained' : 'outlined' }
2023-05-28 12:16:06 +02:00
startIcon = {< FilterListIcon />}
2024-04-07 15:22:03 +02:00
onClick = {() => updateContentType ( SourceContentType . SEARCH , query )}
2023-05-28 12:16:06 +02:00
>
2026-01-10 19:45:02 +01:00
{ t `Filter` }
2023-05-28 12:16:06 +02:00
</ ContentTypeButton >
</ ContentTypeMenu >
2024-10-06 16:15:01 +02:00
{( isLoading || ! error || ( !! error && !! mangas . length )) && (
< BaseMangaGrid
2025-05-17 19:53:56 +02:00
// the key needs to include filters and query to force a re-render of the virtuoso grid to prevent https://github.com/petyosi/react-virtuoso/issues/1242
key = { gridKey }
2024-10-06 16:15:01 +02:00
gridWrapperProps = {{ sx : { px : 1 , pb : 1 } }}
mangas = { mangas }
hasNextPage = { hasNextPage }
loadMore = { loadMore }
message = { message }
messageExtra = { messageExtra }
isLoading = { isLoading }
gridLayout = { sourceGridLayout }
mode = "source"
inLibraryIndicator
/>
)}
2024-12-20 19:15:18 +01:00
{ error && (
< EmptyViewComponent
2026-01-10 19:45:02 +01:00
message = { t `Unable to load data` }
2024-12-20 19:15:18 +01:00
messageExtra = { getErrorMessage ( error )}
2024-10-06 16:15:01 +02:00
retry = {() => loadPage ( lastPageNum ). catch ( defaultPromiseErrorHandler ( 'SourceMangas::refetch' ))}
/>
)}
2024-01-11 02:57:56 +01:00
{ contentType === SourceContentType . SEARCH && (
2022-03-04 01:25:10 +00:00
< SourceOptions
2024-05-08 04:01:23 +02:00
savedSearches = { savedSearches }
selectSavedSearch = { selectSavedSearch }
updateSavedSearches = { handleSavedSearchesUpdate }
2023-05-28 12:16:06 +02:00
sourceFilter = { filters }
updateFilterValue = { setDialogFiltersToApply }
setTriggerUpdate = {() => {
setFiltersToApply ( dialogFiltersToApply );
}}
resetFilterValue = { resetFilters }
update = { dialogFiltersToApply }
2022-03-04 01:25:10 +00:00
/>
)}
2023-05-28 12:16:06 +02:00
</ StyledGridWrapper >
2021-01-22 21:11:00 +03:30
);
2021-01-19 14:47:07 +03:30
}