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' ;
import { styled } 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-05-03 12:14:05 +02:00
import { EmptyViewAbsoluteCentered } from '@/modules/core/components/feedback/EmptyViewAbsoluteCentered.tsx' ;
import { LoadingPlaceholder } from '@/modules/core/components/feedback/LoadingPlaceholder.tsx' ;
2024-10-05 16:09:34 +02:00
import { TabPanel } from '@/modules/core/components/tabs/TabPanel.tsx' ;
2024-10-05 19:33:09 +02:00
import { LibraryToolbarMenu } from '@/modules/library/components/LibraryToolbarMenu.tsx' ;
import { LibraryMangaGrid } from '@/modules/library/components/LibraryMangaGrid.tsx' ;
2024-10-05 16:09:34 +02:00
import { AppbarSearch } from '@/modules/core/components/AppbarSearch.tsx' ;
import { UpdateChecker } from '@/modules/core/components/UpdateChecker.tsx' ;
2024-10-05 19:24:45 +02:00
import { useSelectableCollection } from '@/modules/collection/hooks/useSelectableCollection.ts' ;
import { SelectableCollectionSelectMode } from '@/modules/collection/components/SelectableCollectionSelectMode.tsx' ;
2024-10-05 19:33:09 +02:00
import { useGetVisibleLibraryMangas } from '@/modules/library/hooks/useGetVisibleLibraryMangas.ts' ;
2024-10-05 19:24:45 +02:00
import { SelectionFAB } from '@/modules/collection/components/SelectionFAB.tsx' ;
2024-10-05 19:21:26 +02:00
import { MangaActionMenuItems } from '@/modules/manga/components/MangaActionMenuItems.tsx' ;
2024-10-05 16:09:34 +02:00
import { TabsMenu } from '@/modules/core/components/tabs/TabsMenu.tsx' ;
import { TabsWrapper } from '@/modules/core/components/tabs/TabsWrapper.tsx' ;
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' ;
2024-07-01 21:40:27 +02:00
import { GET_CATEGORIES_LIBRARY } from '@/lib/graphql/queries/CategoryQuery.ts' ;
2024-10-05 19:21:26 +02:00
import { Mangas } from '@/modules/manga/services/Mangas.ts' ;
2024-07-08 18:02:50 +02:00
import { MANGA_CHAPTER_STAT_FIELDS } from '@/lib/graphql/fragments/MangaFragments.ts' ;
2024-10-05 22:53:22 +02:00
import { useMetadataServerSettings } from '@/modules/settings/services/ServerSettingsMetadata.ts' ;
2024-11-28 12:20:36 +01:00
import { GET_LIBRARY_MANGA_COUNT } from '@/lib/graphql/queries/MangaQuery.ts' ;
2025-05-03 13:00:03 +02:00
import { useAppTitle } from '@/modules/navigation-bar/hooks/useAppTitle.ts' ;
2025-05-03 13:24:55 +02:00
import { useAppAction } from '@/modules/navigation-bar/hooks/useAppAction.ts' ;
2025-05-21 00:58:28 +02:00
import { AppRoutes } from '@/modules/core/AppRoute.constants.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 ( ) ;
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
const [ tabSearchParam , setTabSearchParam ] = useQueryParam ( 'tab' , NumberParam ) ;
2025-05-21 00:58:28 +02:00
const [ query ] = useQueryParam ( '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' ) }
{ showTabSize && < TitleSizeTag sx = { { color : 'inherit' } } label = { librarySize } / > }
< / 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 ) ) ] )
2023-12-25 21:37:45 +01: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-08-29 20:47:57 +04: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 = {
mangaError ? t ( 'manga.error.label.request_failure' ) : t ( 'library.error.label.empty' )
}
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
}