/* * Copyright (C) Contributors to the Suwayomi project * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import { Chip, Tab, styled } from '@mui/material'; import React, { useContext, useEffect, useMemo, useState } from 'react'; import { useQueryParam, NumberParam } from 'use-query-params'; import { useTranslation } from 'react-i18next'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { EmptyView } from '@/components/util/EmptyView'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder'; import { TabPanel } from '@/components/tabs/TabPanel.tsx'; import { LibraryToolbarMenu } from '@/components/library/LibraryToolbarMenu'; import { LibraryMangaGrid } from '@/components/library/LibraryMangaGrid'; import { AppbarSearch } from '@/components/util/AppbarSearch'; import { UpdateChecker } from '@/components/library/UpdateChecker'; import { useLibraryOptionsContext } from '@/components/context/LibraryOptionsContext'; import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { useSelectableCollection } from '@/components/collection/useSelectableCollection.ts'; import { TManga } from '@/typings.ts'; import { SelectableCollectionSelectMode } from '@/components/collection/SelectableCollectionSelectMode.tsx'; import { useGetVisibleLibraryMangas } from '@/components/library/useGetVisibleLibraryMangas.ts'; import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx'; import { PARTIAL_MANGA_FIELDS } from '@/lib/graphql/Fragments.ts'; import { MangaActionMenuItems } from '@/components/manga/MangaActionMenuItems.tsx'; import { TabsMenu } from '@/components/tabs/TabsMenu.tsx'; import { TabsWrapper } from '@/components/tabs/TabsWrapper.tsx'; const TitleWithSizeTag = styled('span')({ display: 'flex', alignItems: 'center', }); const TitleSizeTag = (props: React.ComponentProps) => ( ); export function Library() { const { t } = useTranslation(); const { options } = useLibraryOptionsContext(); const { data: categoriesResponse, error: tabsError, loading: areCategoriesLoading, } = requestManager.useGetCategories(); const tabsData = categoriesResponse?.categories.nodes.filter( (category) => category.id !== 0 || (category.id === 0 && category.mangas.totalCount), ); const tabs = tabsData ?? []; const librarySize = useMemo( () => tabs.map((tab) => tab.mangas.totalCount).reduce((prev, curr) => prev + curr, 0), [tabs], ); const [tabSearchParam, setTabSearchParam] = useQueryParam('tab', NumberParam); const activeTab = tabs.find((tab) => tab.order === tabSearchParam) ?? tabs[0]; const { data: categoryMangaResponse, error: mangaError, loading: mangaLoading, } = requestManager.useGetCategoryMangas(activeTab?.id, { skip: !activeTab }); const categoryMangas = categoryMangaResponse?.mangas.nodes ?? []; const { visibleMangas: mangas, showFilteredOutMessage } = useGetVisibleLibraryMangas(categoryMangas); const [isSelectModeActive, setIsSelectModeActive] = useState(false); const { areNoItemsForKeySelected: areNoItemsSelected, areAllItemsForKeySelected: areAllItemsSelected, selectedItemIds, handleSelectAll, handleSelection, clearSelection, } = useSelectableCollection(mangas.length, { currentKey: activeTab?.id.toString() }); const handleSelect = (id: number, selected: boolean) => { setIsSelectModeActive(!!(selectedItemIds.length + (selected ? 1 : -1))); handleSelection(id, selected); }; const selectedMangas = useMemo( () => selectedItemIds.map( (id) => requestManager.graphQLClient.client.cache.readFragment({ id: requestManager.graphQLClient.client.cache.identify({ __typename: 'MangaType', id }), fragment: PARTIAL_MANGA_FIELDS, fragmentName: 'PARTIAL_MANGA_FIELDS', })!, ), [selectedItemIds.length, mangas], ); const selectionFab = useMemo(() => { if (!isSelectModeActive) { return null; } return ( {(handleClose, setHideMenu) => ( { handleClose(); setIsSelectModeActive(selectionModeState); if (!selectionModeState) { clearSelection(); } }} setHideMenu={setHideMenu} /> )} ); }, [isSelectModeActive, selectedMangas]); const { setTitle, setAction } = useContext(NavBarContext); useEffect(() => { const title = t('library.title'); const navBarTitle = ( {title} {options.showTabSize && } ); setTitle(navBarTitle, title); setAction( <> {!isSelectModeActive && ( <> )} handleSelectAll(selectAll, [...new Set(mangas.map((manga) => manga.id))]) } onModeChange={(checked) => { setIsSelectModeActive(checked); if (checked) { handleSelectAll(true, [...new Set(mangas.map((manga) => manga.id))]); } else { tabs.forEach((tab) => handleSelectAll(false, [], tab.id.toString())); } }} /> , ); return () => { setTitle(''); setAction(null); }; }, [ t, librarySize, areCategoriesLoading, options, isSelectModeActive, areNoItemsSelected, areAllItemsSelected, selectedItemIds.length, mangas.length, ]); const handleTabChange = (newTab: number) => { setTabSearchParam(newTab); }; if (tabsError != null) { return ( ); } if (areCategoriesLoading) { return ; } if (tabs.length === 0) { return ; } if (tabs.length === 1) { return ( <> {selectionFab} ); } return ( handleTabChange(newTab)} tabsCount={tabs.length}> {tabs.map((tab) => ( {tab.name} {options.showTabSize ? : null} } value={tab.order} /> ))} {tabs.map((tab) => ( {tab === activeTab && (mangaError ? ( ) : ( ))} ))} {selectionFab} ); }