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-02-13 21:12:18 +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-02-13 21:12:18 +03:30
2024-09-06 16:59:27 +02:00
import Chip , { ChipProps } from '@mui/material/Chip' ;
2024-04-14 15:50:12 +02:00
import Tab from '@mui/material/Tab' ;
2026-01-08 16:09:50 +01:00
import { styled , useTheme } from '@mui/material/styles' ;
2025-05-03 13:59:21 +02:00
import { useCallback , useMemo , useState } from 'react' ;
2025-05-21 00:58:28 +02:00
import { useQueryParam , NumberParam , StringParam } from 'use-query-params' ;
2023-02-13 20:20:08 +03:30
import { useTranslation } from 'react-i18next' ;
2025-05-21 00:58:28 +02:00
import Button from '@mui/material/Button' ;
import { Link } from 'react-router-dom' ;
import Box from '@mui/material/Box' ;
2024-10-06 01:27:51 +02:00
import { requestManager } from '@/lib/requests/RequestManager.ts' ;
2025-08-16 02:48:46 +02:00
import { EmptyViewAbsoluteCentered } from '@/base/components/feedback/EmptyViewAbsoluteCentered.tsx' ;
import { LoadingPlaceholder } from '@/base/components/feedback/LoadingPlaceholder.tsx' ;
import { TabPanel } from '@/base/components/tabs/TabPanel.tsx' ;
2025-08-15 22:02:58 +02:00
import { LibraryToolbarMenu } from '@/features/library/components/LibraryToolbarMenu.tsx' ;
import { LibraryMangaGrid } from '@/features/library/components/LibraryMangaGrid.tsx' ;
2025-08-16 02:48:46 +02:00
import { AppbarSearch } from '@/base/components/AppbarSearch.tsx' ;
2025-08-15 22:07:33 +02:00
import { UpdateChecker } from '@/features/updates/components/UpdateChecker.tsx' ;
2025-10-04 00:14:11 +02:00
import { useSelectableCollection } from '@/base/collection/hooks/useSelectableCollection.ts' ;
import { SelectableCollectionSelectMode } from '@/base/collection/components/SelectableCollectionSelectMode.tsx' ;
2025-08-15 22:02:58 +02:00
import { useGetVisibleLibraryMangas } from '@/features/library/hooks/useGetVisibleLibraryMangas.ts' ;
2025-10-04 00:14:11 +02:00
import { SelectionFAB } from '@/base/collection/components/SelectionFAB.tsx' ;
2025-08-15 22:02:58 +02:00
import { MangaActionMenuItems } from '@/features/manga/components/MangaActionMenuItems.tsx' ;
2025-08-16 02:48:46 +02:00
import { TabsMenu } from '@/base/components/tabs/TabsMenu.tsx' ;
import { TabsWrapper } from '@/base/components/tabs/TabsWrapper.tsx' ;
2024-10-05 16:09:34 +02:00
import { defaultPromiseErrorHandler } from '@/lib/DefaultPromiseErrorHandler.ts' ;
2024-07-08 18:02:50 +02:00
import {
GetCategoriesLibraryQuery ,
GetCategoriesLibraryQueryVariables ,
2024-11-28 12:20:36 +01:00
GetLibraryMangaCountQuery ,
GetLibraryMangaCountQueryVariables ,
2024-07-08 18:02:50 +02:00
MangaChapterStatFieldsFragment ,
MangaType ,
} from '@/lib/graphql/generated/graphql.ts' ;
2025-12-22 14:00:44 +01:00
import { GET_CATEGORIES_LIBRARY } from '@/lib/graphql/category/CategoryQuery.ts' ;
2025-08-15 22:02:58 +02:00
import { Mangas } from '@/features/manga/services/Mangas.ts' ;
2025-12-22 14:00:44 +01:00
import { MANGA_CHAPTER_STAT_FIELDS } from '@/lib/graphql/manga/MangaFragments.ts' ;
2025-08-15 22:02:58 +02:00
import { useMetadataServerSettings } from '@/features/settings/services/ServerSettingsMetadata.ts' ;
2025-12-22 14:00:44 +01:00
import { GET_LIBRARY_MANGA_COUNT } from '@/lib/graphql/manga/MangaQuery.ts' ;
2025-08-15 22:02:58 +02:00
import { useAppTitle } from '@/features/navigation-bar/hooks/useAppTitle.ts' ;
import { useAppAction } from '@/features/navigation-bar/hooks/useAppAction.ts' ;
2025-08-16 02:48:46 +02:00
import { AppRoutes } from '@/base/AppRoute.constants.ts' ;
import { SearchParam } from '@/base/Base.types.ts' ;
2023-06-17 16:55:24 +02:00
2023-04-22 11:39:59 +02:00
const TitleWithSizeTag = styled ( 'span' )({
display : 'flex' ,
alignItems : 'center' ,
});
2024-09-06 16:59:27 +02:00
const TitleSizeTag = ({ sx , ... props } : ChipProps ) => (
< Chip { ...props } size = "small" sx = {{ ... sx , marginLeft : '5px' }} />
2023-04-22 11:39:59 +02:00
);
2021-02-13 21:12:18 +03:30
2023-10-28 00:32:02 +02:00
export function Library() {
2023-02-13 20:20:08 +03:30
const { t } = useTranslation ();
2026-01-08 16:09:50 +01:00
const theme = useTheme ();
2023-02-14 16:47:38 +03:30
2024-09-17 03:40:55 +02:00
const {
settings : { showTabSize },
} = useMetadataServerSettings ();
2023-09-06 21:47:13 +02:00
const {
data : categoriesResponse ,
error : tabsError ,
loading : areCategoriesLoading ,
2024-04-27 22:29:23 +02:00
refetch : refetchCategories ,
2024-07-01 21:40:27 +02:00
} = requestManager . useGetCategories < GetCategoriesLibraryQuery , GetCategoriesLibraryQueryVariables >(
GET_CATEGORIES_LIBRARY ,
{
notifyOnNetworkStatusChange : true ,
},
);
2023-09-06 21:47:13 +02:00
const tabsData = categoriesResponse ? . categories . nodes . filter (
( category ) => category . id !== 0 || ( category . id === 0 && category . mangas . totalCount ),
);
2022-11-02 10:42:02 +01:00
const tabs = tabsData ?? [];
2024-11-28 12:20:36 +01:00
const librarySizeResponse = requestManager . useGetMangas <
GetLibraryMangaCountQuery ,
GetLibraryMangaCountQueryVariables
> ( GET_LIBRARY_MANGA_COUNT , {});
const librarySize = librarySizeResponse . data ? . mangas . totalCount ?? 0 ;
2022-11-02 10:42:02 +01:00
2025-07-23 01:52:41 +02:00
const [ tabSearchParam , setTabSearchParam ] = useQueryParam ( SearchParam . TAB , NumberParam );
const [ query ] = useQueryParam ( SearchParam . QUERY , StringParam );
2022-11-02 10:42:02 +01:00
2025-03-09 16:37:12 +01:00
const activeTab : ( typeof tabs )[ number ] | undefined = tabs . find (( tab ) => tab . id === tabSearchParam ) ?? tabs [ 0 ];
2024-09-17 03:40:55 +02:00
2022-11-02 21:48:22 +01:00
const {
2023-09-06 21:47:13 +02:00
data : categoryMangaResponse ,
2022-11-02 21:48:22 +01:00
error : mangaError ,
2023-09-06 21:47:13 +02:00
loading : mangaLoading ,
2024-04-27 22:29:23 +02:00
refetch : refetchCategoryMangas ,
} = requestManager . useGetCategoryMangas ( activeTab ? . id , { skip : ! activeTab , notifyOnNetworkStatusChange : true });
2023-12-25 21:37:45 +01:00
const categoryMangas = categoryMangaResponse ? . mangas . nodes ?? [];
2025-05-17 19:53:56 +02:00
const {
visibleMangas : mangas ,
showFilteredOutMessage ,
filterKey ,
} = useGetVisibleLibraryMangas ( categoryMangas , activeTab );
2023-12-25 21:37:45 +01:00
2024-04-27 22:29:23 +02:00
const retryFetchCategoryMangas = useCallback (
() => refetchCategoryMangas (). catch ( defaultPromiseErrorHandler ( 'Library::refetchCategoryMangas' )),
[ refetchCategoryMangas , activeTab ],
);
2024-04-05 02:03:00 +02:00
const mangaIds = useMemo (() => mangas . map (( manga ) => manga . id ), [ mangas ]);
2023-12-25 21:37:45 +01:00
const [ isSelectModeActive , setIsSelectModeActive ] = useState ( false );
const {
areNoItemsForKeySelected : areNoItemsSelected ,
areAllItemsForKeySelected : areAllItemsSelected ,
selectedItemIds ,
handleSelectAll ,
handleSelection ,
2024-01-21 22:31:43 +01:00
clearSelection ,
2024-07-08 18:02:50 +02:00
} = useSelectableCollection < MangaType [ 'id' ] , string >( mangas . length , {
2024-04-05 02:03:00 +02:00
itemIds : mangaIds ,
currentKey : activeTab?.id.toString (),
});
2023-12-25 21:37:45 +01:00
2025-01-13 01:29:40 +01:00
const handleSelect : typeof handleSelection = useCallback (
( id , selected , selectOptions ) => {
setIsSelectModeActive ( !! ( selectedItemIds . length + ( selected ? 1 : - 1 )));
handleSelection ( id , selected , selectOptions );
},
[ setIsSelectModeActive , handleSelection ],
);
2023-12-25 21:37:45 +01:00
const selectedMangas = useMemo (
() =>
2024-07-08 18:02:50 +02:00
selectedItemIds
. map (( id ) =>
Mangas . getFromCache < MangaChapterStatFieldsFragment >(
id ,
MANGA_CHAPTER_STAT_FIELDS ,
'MANGA_CHAPTER_STAT_FIELDS' ,
),
)
. filter (( manga ) => !! manga ),
2023-12-30 04:04:47 +01:00
[ selectedItemIds . length , mangas ],
2023-12-25 21:37:45 +01:00
);
2022-11-02 10:42:02 +01:00
2024-01-15 21:19:35 +01:00
const selectionFab = useMemo (() => {
if ( ! isSelectModeActive ) {
return null ;
}
return (
< SelectionFAB selectedItemsCount = { selectedItemIds . length } title = "manga.title" >
{( handleClose , setHideMenu ) => (
< MangaActionMenuItems
selectedMangas = { selectedMangas }
2024-06-13 12:17:02 +02:00
onClose = {() => {
2024-01-15 21:19:35 +01:00
handleClose ();
2024-06-13 12:17:02 +02:00
setIsSelectModeActive ( false );
clearSelection ();
2024-01-15 21:19:35 +01:00
}}
setHideMenu = { setHideMenu }
/>
)}
</ SelectionFAB >
);
}, [ isSelectModeActive , selectedMangas ]);
2025-05-21 00:58:28 +02:00
const triggerGlobalSearchButton = useMemo (
2025-05-21 01:18:31 +02:00
() =>
!! query && (
< Box sx = {{ p : 2 }}>
< Button
size = "large"
component = { Link }
to = { AppRoutes . sources . childRoutes . searchAll . path ( query )}
sx = {{ textTransform : 'none' , width : '100%' }}
>
{ t ( 'library.action.label.search_globally' , { query })}
</ Button >
</ Box >
),
2025-05-21 00:58:28 +02:00
[ query ],
);
2025-05-03 13:00:03 +02:00
useAppTitle (
< TitleWithSizeTag >
{ t ( 'library.title' )}
2026-01-08 16:09:50 +01:00
{ showTabSize && (
< TitleSizeTag
sx = {{ ... theme . applyStyles ( 'light' , { backgroundColor : 'background.paper' }) }}
label = { librarySize }
/>
)}
2025-05-03 13:00:03 +02:00
</ TitleWithSizeTag >,
t ( 'library.title' ),
[ t , showTabSize , librarySize ],
);
2025-05-03 13:24:55 +02:00
useAppAction (
<>
{ ! isSelectModeActive && activeTab && (
<>
< AppbarSearch />
< LibraryToolbarMenu category = { activeTab } />
< UpdateChecker categoryId = { activeTab ? . id } />
</>
)}
2025-05-21 00:43:03 +02:00
{ !! mangas . length && (
< SelectableCollectionSelectMode
isActive = { isSelectModeActive }
areAllItemsSelected = { areAllItemsSelected }
areNoItemsSelected = { areNoItemsSelected }
onSelectAll = {( selectAll ) =>
handleSelectAll ( selectAll , [... new Set ( mangas . map (( manga ) => manga . id ))])
2025-05-03 13:24:55 +02:00
}
2025-05-21 00:43:03 +02:00
onModeChange = {( checked ) => {
setIsSelectModeActive ( checked );
if ( checked ) {
handleSelectAll ( true , [... new Set ( mangas . map (( manga ) => manga . id ))]);
} else {
tabs . forEach (( tab ) => handleSelectAll ( false , [], tab . id . toString ()));
}
}}
/>
)}
2025-05-03 13:24:55 +02:00
</>,
2025-05-21 00:43:03 +02:00
[ isSelectModeActive , areNoItemsSelected , areAllItemsSelected , activeTab , mangas . length ],
2025-05-03 13:24:55 +02:00
);
2021-03-09 16:44:09 +03:30
2021-02-21 04:27:41 +03:30
const handleTabChange = ( newTab : number ) => {
2023-09-06 21:47:13 +02:00
setTabSearchParam ( newTab );
2021-02-21 04:27:41 +03:30
};
2024-11-28 12:20:36 +01:00
if ( tabsError != null || librarySizeResponse . error ) {
2023-02-14 16:28:37 +03:30
return (
2024-04-29 15:29:33 +02:00
< EmptyViewAbsoluteCentered
2024-11-28 12:20:36 +01:00
message = { t ( 'global.error.label.failed_to_load_data' )}
messageExtra = { tabsError ? . message ?? librarySizeResponse . error ? . message }
retry = {() => {
if ( tabsError ) {
refetchCategories (). catch ( defaultPromiseErrorHandler ( 'Library::refetchCategories' ));
}
if ( librarySizeResponse . error ) {
librarySizeResponse . refetch (). catch ( defaultPromiseErrorHandler ( 'Library::refetchLibrarySize' ));
}
}}
2023-02-14 16:28:37 +03:30
/>
);
2022-11-02 10:42:02 +01:00
}
2021-02-20 01:23:52 +03:30
2024-11-28 12:20:36 +01:00
if ( areCategoriesLoading || librarySizeResponse . loading ) {
2021-10-24 22:56:10 +03:30
return < LoadingPlaceholder />;
2021-08-29 20:47:57 +04:30
}
2021-08-21 03:46:01 +04:30
if ( tabs . length === 0 ) {
2024-04-29 15:29:33 +02:00
return < EmptyViewAbsoluteCentered message = { t ( 'library.error.label.empty' )} />;
2021-08-21 03:46:01 +04:30
}
2021-03-07 16:27:13 +03:30
2022-11-02 10:42:02 +01:00
if ( tabs . length === 1 ) {
return (
2024-01-15 21:19:35 +01:00
<>
2025-05-21 00:58:28 +02:00
{ triggerGlobalSearchButton }
2024-01-15 21:19:35 +01:00
< LibraryMangaGrid
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 = { filterKey }
2024-01-15 21:19:35 +01:00
mangas = { mangas }
2024-04-27 22:29:23 +02:00
message = { mangaError ? t ( 'manga.error.label.request_failure' ) : t ( 'library.error.label.empty' )}
messageExtra = { mangaError ? . message }
isLoading = { mangaLoading }
2024-01-15 21:19:35 +01:00
selectedMangaIds = { selectedItemIds }
isSelectModeActive = { isSelectModeActive }
handleSelection = { handleSelect }
2024-04-27 22:29:23 +02:00
showFilteredOutMessage = { ! mangaError && showFilteredOutMessage }
retry = { mangaError && retryFetchCategoryMangas }
2024-01-15 21:19:35 +01:00
/>
{ selectionFab }
</>
2021-02-20 01:23:52 +03:30
);
}
2022-11-02 10:42:02 +01:00
return (
2024-01-15 21:19:35 +01:00
< TabsWrapper >
2025-03-09 16:37:12 +01:00
< TabsMenu value = { activeTab . id } onChange = {( e , newTab ) => handleTabChange ( newTab )}>
2022-11-02 10:42:02 +01:00
{ tabs . map (( tab ) => (
2024-01-15 21:19:35 +01:00
< Tab
2024-11-04 18:00:35 +01:00
sx = {{ flexGrow : 1 , maxWidth : 'unset' }}
2024-01-15 21:19:35 +01:00
key = { tab . id }
label = {
< TitleWithSizeTag >
{ tab . name }
2024-09-17 03:40:55 +02:00
{ showTabSize ? < TitleSizeTag label = { tab . mangas . totalCount } /> : null }
2024-01-15 21:19:35 +01:00
</ TitleWithSizeTag >
}
2025-03-09 16:37:12 +01:00
value = { tab . id }
2024-01-15 21:19:35 +01:00
/>
2022-11-02 10:42:02 +01:00
))}
2024-01-15 21:19:35 +01:00
</ TabsMenu >
2025-05-21 00:58:28 +02:00
{ triggerGlobalSearchButton }
2024-01-15 21:19:35 +01:00
{ tabs . map (( tab ) => (
< TabPanel key = { tab . order } index = { tab . order } currentIndex = { activeTab . order }>
2024-04-27 22:29:23 +02:00
{ tab === activeTab && (
< LibraryMangaGrid
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 = { filterKey }
2024-04-27 22:29:23 +02:00
mangas = { mangas }
message = {
2025-08-13 21:37:36 +02:00
mangaError ? t ( 'manga.error.label.request_failure' ) : t ( 'category.error.label.empty' )
2024-04-27 22:29:23 +02:00
}
messageExtra = { mangaError ? . message }
isLoading = { mangaLoading }
selectedMangaIds = { selectedItemIds }
isSelectModeActive = { isSelectModeActive }
handleSelection = { handleSelect }
showFilteredOutMessage = { ! mangaError && showFilteredOutMessage }
retry = { mangaError && retryFetchCategoryMangas }
/>
)}
2024-01-15 21:19:35 +01:00
</ TabPanel >
))}
{ selectionFab }
</ TabsWrapper >
2022-11-02 10:42:02 +01:00
);
2021-02-13 21:12:18 +03:30
}