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
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-03-23 13:59:32 +01:00
import { useTranslation } from 'react-i18next' ;
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' ;
2025-02-01 14:24:38 +01:00
import { CustomTooltip } from '@/modules/core/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' ;
2024-10-05 21:22:12 +02:00
import { SourceGridLayout } from '@/modules/source/components/SourceGridLayout.tsx' ;
2024-10-05 16:09:34 +02:00
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx' ;
2024-10-05 21:22:12 +02:00
import { SourceOptions } from '@/modules/source/components/SourceOptions.tsx' ;
import { BaseMangaGrid } from '@/modules/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 ,
} from '@/modules/settings/services/ServerSettingsMetadata.ts' ;
2024-10-05 16:09:34 +02:00
import { useLocalStorage , useSessionStorage } from '@/modules/core/hooks/useStorage.tsx' ;
2025-05-25 15:36:25 +02:00
import { MANGA_GRID_SNAPSHOT_KEY } from '@/modules/manga/components/MangaGrid.tsx' ;
2024-10-26 17:25:04 +02:00
import { createUpdateSourceMetadata , useGetSourceMetadata } from '@/modules/source/services/SourceMetadata.ts' ;
2024-10-05 23:22:42 +02:00
import { makeToast } from '@/modules/core/utils/Toast.ts' ;
2024-07-03 23:15:50 +02:00
import { GET_SOURCE_BROWSE } from '@/lib/graphql/queries/SourceQuery.ts' ;
2024-10-05 16:09:34 +02:00
import { TranslationKey } from '@/Base.types.ts' ;
2025-04-21 17:00:13 +02:00
import { IPos , SourceIdInfo } from '@/modules/source/Source.types.ts' ;
2024-10-06 16:15:01 +02:00
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' ;
2025-05-03 12:14:05 +02:00
import { EmptyView } from '@/modules/core/components/feedback/EmptyView.tsx' ;
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/feedback/EmptyViewAbsoluteCentered.tsx' ;
2024-10-07 13:35:07 +02:00
import { MangaIdInfo } from '@/modules/manga/Manga.types.ts' ;
2025-07-23 01:52:41 +02:00
import { GridLayout , SearchParam } from '@/modules/core/Core.types.ts' ;
2024-12-11 20:10:59 +01:00
import { AppRoutes } from '@/modules/core/AppRoute.constants.ts' ;
2024-12-20 17:24:40 +01:00
import { getErrorMessage } from '@/lib/HelperFunctions.ts' ;
2025-04-21 17:00:13 +02:00
import { Sources } from '@/modules/source/services/Sources.ts' ;
2025-05-03 13:37:20 +02:00
import { useAppTitleAndAction } from '@/modules/navigation-bar/hooks/useAppTitleAndAction.ts' ;
2025-05-03 13:24:55 +02:00
import { useNavBarContext } from '@/modules/navigation-bar/contexts/NavbarContext.tsx' ;
2025-05-25 15:36:25 +02:00
import { VirtuosoUtil } from '@/lib/virtuoso/Virtuoso.util.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
2023-05-28 12:16:06 +02:00
const SOURCE_CONTENT_TYPE_TO_ERROR_MSG_KEY : { [ contentType in SourceContentType ] : TranslationKey } = {
[ SourceContentType . POPULAR ] : 'manga.error.label.no_mangas_found' ,
[ SourceContentType . LATEST ] : 'manga.error.label.no_mangas_found' ,
2024-01-11 02:57:56 +01:00
[ SourceContentType . SEARCH ] : 'manga.error.label.no_matches' ,
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() {
2023-03-23 13:59:32 +01:00
const { t } = useTranslation ();
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-03-23 23:44:46 +01:00
const isLoading = loading || filteredOutAllItemsOfFetchedPage ;
2024-06-02 15:24:29 +02:00
const mangas = data ? . fetchSourceManga ? . mangas ?? [];
const hasNextPage = !! data ? . fetchSourceManga ? . 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 ) =>
makeToast ( t ( 'global.error.label.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 ? (
<>
< span >{ t ( 'source.local_source.label.checkout' )} </ span >
2024-05-04 23:54:33 +02:00
< Link href = "https://github.com/Suwayomi/Suwayomi-Server/wiki/Local-Source" target = "_blank" rel = "noreferrer" >
2023-05-28 12:16:06 +02:00
{ t ( 'source.local_source.label.guide' )}
</ 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 ;
}
const requiresClear = SPECIAL_ED_SOURCES . REVALIDATION . includes ( sourceId );
if ( ! requiresClear ) {
return ;
}
requestManager . clearBrowseCacheFor ( sourceId );
}, [ clearCache ]);
2025-05-03 13:37:20 +02:00
useAppTitleAndAction (
source ? . displayName ?? t ( 'source.title_one' ),
2025-05-03 13:24:55 +02:00
<>
< AppbarSearch />
< SourceGridLayout />
{ source ? . isConfigurable && (
< CustomTooltip title = { t ( 'settings.title' )}>
< 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 )}
>
{ t ( 'global.button.popular' )}
</ 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 )}
>
{ t ( 'global.button.latest' )}
</ 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
>
{ t ( 'global.button.filter' )}
</ 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
2024-10-06 16:15:01 +02:00
message = { t ( 'global.error.label.failed_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
}