diff --git a/src/components/ExtensionCard.tsx b/src/components/ExtensionCard.tsx index 26f7c48e..3257dd42 100644 --- a/src/components/ExtensionCard.tsx +++ b/src/components/ExtensionCard.tsx @@ -148,13 +148,16 @@ export function ExtensionCard(props: IProps) { } return ( - + @@ -173,10 +176,10 @@ export function ExtensionCard(props: IProps) { {name} - + {langPress} {versionName} {isNsfw && ( - + {' 18+'} )} diff --git a/src/components/SourceCard.tsx b/src/components/SourceCard.tsx index a34bbd10..8c22291b 100644 --- a/src/components/SourceCard.tsx +++ b/src/components/SourceCard.tsx @@ -53,6 +53,7 @@ export const SourceCard: React.FC = (props: IProps) => { { diff --git a/src/components/navbar/DefaultNavBar.tsx b/src/components/navbar/DefaultNavBar.tsx index 570e49db..edd1e4b3 100644 --- a/src/components/navbar/DefaultNavBar.tsx +++ b/src/components/navbar/DefaultNavBar.tsx @@ -19,7 +19,6 @@ import NewReleasesIcon from '@mui/icons-material/NewReleases'; import NewReleasesOutlinedIcon from '@mui/icons-material/NewReleasesOutlined'; import ExploreIcon from '@mui/icons-material/Explore'; import ExploreOutlinedIcon from '@mui/icons-material/ExploreOutlined'; -import ExtensionIcon from '@mui/icons-material/Extension'; import GetAppIcon from '@mui/icons-material/GetApp'; import GetAppOutlinedIcon from '@mui/icons-material/GetAppOutlined'; import SettingsIcon from '@mui/icons-material/Settings'; @@ -28,7 +27,6 @@ import { useLocation } from 'react-router-dom'; import { createPortal } from 'react-dom'; import { NavbarItem } from '@/typings'; import { NavBarContext } from '@/components/context/NavbarContext'; -import { ExtensionOutlinedIcon } from '@/components/util/CustomExtensionOutlinedIcon'; import { DesktopSideBar } from '@/components/navbar/navigation/DesktopSideBar'; import { MobileBottomBar } from '@/components/navbar/navigation/MobileBottomBar'; import { useBackButton } from '@/util/useBackButton.ts'; @@ -48,26 +46,12 @@ const navbarItems: Array = [ IconComponent: NewReleasesOutlinedIcon, show: 'both', }, - { - path: '/extensions', - title: 'extension.title', - SelectedIconComponent: ExtensionIcon, - IconComponent: ExtensionOutlinedIcon, - show: 'desktop', - }, - { - path: '/sources', - title: 'source.title', - SelectedIconComponent: ExploreIcon, - IconComponent: ExploreOutlinedIcon, - show: 'desktop', - }, { path: '/browse', title: 'global.label.browse', SelectedIconComponent: ExploreIcon, IconComponent: ExploreOutlinedIcon, - show: 'mobile', + show: 'both', }, { path: '/downloads', diff --git a/src/components/util/TabPanel.tsx b/src/components/tabs/TabPanel.tsx similarity index 100% rename from src/components/util/TabPanel.tsx rename to src/components/tabs/TabPanel.tsx diff --git a/src/components/tabs/TabsMenu.tsx b/src/components/tabs/TabsMenu.tsx new file mode 100644 index 00000000..2391e276 --- /dev/null +++ b/src/components/tabs/TabsMenu.tsx @@ -0,0 +1,45 @@ +/* + * 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 { styled, Tabs, TabsProps } from '@mui/material'; + +const StyledTabsMenu = styled(Tabs)(({ theme }) => ({ + display: 'flex', + position: 'fixed', + top: '64px', + width: 'calc(100% - 64px)', + zIndex: 1, + backgroundColor: theme.palette.background.default, + border: 0, + borderBottomWidth: 2, + borderStyle: 'solid', + borderColor: theme.palette.divider, + [theme.breakpoints.down('sm')]: { + top: '56px', // header height + width: '100%', + }, +})); + +export const TabsMenu = ({ children, tabsCount, ...props }: TabsProps & { tabsCount: number }) => { + // Visual Hack: 160px is min-width for viewport width of >600 + const scrollableTabs = window.innerWidth < tabsCount * 160; + + return ( + + {children} + + ); +}; diff --git a/src/components/tabs/TabsWrapper.tsx b/src/components/tabs/TabsWrapper.tsx new file mode 100644 index 00000000..6b98a10e --- /dev/null +++ b/src/components/tabs/TabsWrapper.tsx @@ -0,0 +1,23 @@ +/* + * 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 { Box, styled } from '@mui/material'; + +export const TabsWrapper = styled(Box)(({ theme }) => ({ + // TabsMenu height + TabsMenu bottom padding - grid item top padding + marginTop: `calc(48px + 13px - 8px)`, + // header height - TabsMenu height - TabsMenu bottom padding + grid item top padding + minHeight: 'calc(100vh - 64px - 48px - 13px + 8px)', + position: 'relative', + [theme.breakpoints.down('sm')]: { + // TabsMenu - 8px margin diff header height (56px) + TabsMenu bottom padding - grid item top padding + marginTop: `calc(48px - 8px + 13px - 8px)`, + // header height (+ 8px margin) - footer height - TabsMenu height + minHeight: 'calc(100vh - 64px - 64px - 48px)', + }, +})); diff --git a/src/components/virtuoso/StyledGroupHeader.tsx b/src/components/virtuoso/StyledGroupHeader.tsx new file mode 100644 index 00000000..b68e547c --- /dev/null +++ b/src/components/virtuoso/StyledGroupHeader.tsx @@ -0,0 +1,26 @@ +/* + * 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 { styled } from '@mui/material'; +import Typography from '@mui/material/Typography'; + +export const StyledGroupHeader = styled(Typography, { shouldForwardProp: (prop) => prop !== 'isFirstItem' })<{ + isFirstItem: boolean; +}>(({ theme, isFirstItem }) => ({ + paddingLeft: '24px', + // 16px - 10px (bottom padding of the group items) + paddingTop: '6px', + paddingBottom: '16px', + fontWeight: 700, + textTransform: 'uppercase', + backgroundColor: theme.palette.background.default, + [theme.breakpoints.down('sm')]: { + // 16px - 8px (margin of header) + paddingTop: isFirstItem ? '8px' : '6px', + }, +})); diff --git a/src/components/virtuoso/StyledGroupItemWrapper.tsx b/src/components/virtuoso/StyledGroupItemWrapper.tsx new file mode 100644 index 00000000..b144d1a9 --- /dev/null +++ b/src/components/virtuoso/StyledGroupItemWrapper.tsx @@ -0,0 +1,16 @@ +/* + * 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 { Box, styled } from '@mui/material'; + +export const StyledGroupItemWrapper = styled(Box, { shouldForwardProp: (prop) => prop !== 'isLastItem' })<{ + isLastItem: boolean; +}>(({ isLastItem }) => ({ + padding: '0 10px', + paddingBottom: isLastItem ? '0' : '10px', +})); diff --git a/src/components/virtuoso/StyledGroupedVirtuoso.tsx b/src/components/virtuoso/StyledGroupedVirtuoso.tsx new file mode 100644 index 00000000..6f1aada3 --- /dev/null +++ b/src/components/virtuoso/StyledGroupedVirtuoso.tsx @@ -0,0 +1,23 @@ +/* + * 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 { styled } from '@mui/material'; +import { GroupedVirtuoso } from 'react-virtuoso'; + +export const StyledGroupedVirtuoso = styled(GroupedVirtuoso, { + shouldForwardProp: (prop) => prop !== 'heightToSubtract', +})<{ + heightToSubtract?: number; +}>(({ theme, heightToSubtract = 0 }) => ({ + // 64px header + height: `calc(100vh - 64px - ${heightToSubtract}px)`, + [theme.breakpoints.down('sm')]: { + // 64px header (margin); 64px menu (margin); + height: `calc(100vh - 64px - 64px - ${heightToSubtract}px)`, + }, +})); diff --git a/src/screens/Browse.tsx b/src/screens/Browse.tsx index 78937e40..f1bf5cb5 100644 --- a/src/screens/Browse.tsx +++ b/src/screens/Browse.tsx @@ -7,12 +7,13 @@ */ import { useState } from 'react'; -import Tabs from '@mui/material/Tabs'; import Tab from '@mui/material/Tab'; import { useTranslation } from 'react-i18next'; import { Sources } from '@/screens/Sources'; import { Extensions } from '@/screens/Extensions'; -import { TabPanel } from '@/components/util/TabPanel'; +import { TabPanel } from '@/components/tabs/TabPanel.tsx'; +import { TabsWrapper } from '@/components/tabs/TabsWrapper.tsx'; +import { TabsMenu } from '@/components/tabs/TabsMenu.tsx'; export function Browse() { const { t } = useTranslation(); @@ -20,26 +21,17 @@ export function Browse() { const [tabNum, setTabNum] = useState(0); return ( - <> - setTabNum(newTab)} - indicatorColor="primary" - textColor="primary" - centered - variant="fullWidth" - scrollButtons - allowScrollButtonsMobile - > + + setTabNum(newTab)}> - + - + ); } diff --git a/src/screens/Extensions.tsx b/src/screens/Extensions.tsx index 598748b3..7fa84355 100644 --- a/src/screens/Extensions.tsx +++ b/src/screens/Extensions.tsx @@ -11,9 +11,9 @@ import { fromEvent } from 'file-selector'; import IconButton from '@mui/material/IconButton'; import AddIcon from '@mui/icons-material/Add'; import { StringParam, useQueryParam } from 'use-query-params'; -import { Virtuoso } from 'react-virtuoso'; -import { Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material'; +import { Tooltip, useMediaQuery } from '@mui/material'; import { useTranslation } from 'react-i18next'; +import { useTheme } from '@mui/material/styles'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { extensionDefaultLangs, DefaultLanguage, langSortCmp } from '@/util/language'; import { useLocalStorage } from '@/util/useLocalStorage'; @@ -31,6 +31,9 @@ import { LangSelect } from '@/components/navbar/action/LangSelect'; import { ExtensionCard } from '@/components/ExtensionCard'; import { PartialExtension } from '@/typings.ts'; import { NavBarContext } from '@/components/context/NavbarContext.tsx'; +import { StyledGroupedVirtuoso } from '@/components/virtuoso/StyledGroupedVirtuoso.tsx'; +import { StyledGroupHeader } from '@/components/virtuoso/StyledGroupHeader.tsx'; +import { StyledGroupItemWrapper } from '@/components/virtuoso/StyledGroupItemWrapper.tsx'; const LANGUAGE = 0; const EXTENSIONS = 1; @@ -92,12 +95,13 @@ function getExtensionsInfo(extensions: PartialExtension[]): { export function Extensions() { const { t } = useTranslation(); + const theme = useTheme(); + const isMobileWidth = useMediaQuery(theme.breakpoints.down('sm')); + const inputRef = useRef(null); const { setTitle, setAction } = useContext(NavBarContext); const [shownLangs, setShownLangs] = useLocalStorage('shownExtensionLangs', extensionDefaultLangs()); const [showNsfw] = useLocalStorage('showNsfw', true); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down('sm')); const [query] = useQueryParam('query', StringParam); const [refetchExtensions, setRefetchExtensions] = useState({}); @@ -130,7 +134,14 @@ export function Extensions() { [shownLangs, groupedExtensions], ); - const flatRenderItems: (PartialExtension | string)[] = filteredGroupedExtensions.flat(2); + const groupCounts = useMemo( + () => filteredGroupedExtensions.map((extensionGroup) => extensionGroup[EXTENSIONS].length - 1), + [filteredGroupedExtensions], + ); + const visibleExtensions = useMemo( + () => filteredGroupedExtensions.map(([, extensions]) => extensions).flat(1), + [filteredGroupedExtensions], + ); const [toasts, makeToast] = makeToaster(useState([])); @@ -206,33 +217,47 @@ export function Extensions() { } }} /> - { - if (typeof flatRenderItems[index] === 'string') { - const item = flatRenderItems[index] as string; - return ( - - {translateExtensionLanguage(item)} - - ); - } - const item = flatRenderItems[index] as PartialExtension; + heightToSubtract={ + isMobileWidth + ? // desktop: TabsMenu height + 48 + : // desktop: TabsMenu height - TabsMenu bottom padding + grid item top padding + 48 + 13 - 8 + } + overscan={window.innerHeight * 0.5} + groupCounts={groupCounts} + groupContent={(index) => { + const [groupName] = filteredGroupedExtensions[index]; - return ; + return ( + + {translateExtensionLanguage(groupName)} + + ); + }} + itemContent={(index) => { + const item = visibleExtensions[index]; + + return ( + + + + ); }} /> diff --git a/src/screens/Library.tsx b/src/screens/Library.tsx index 82ddba2f..1283d190 100644 --- a/src/screens/Library.tsx +++ b/src/screens/Library.tsx @@ -6,14 +6,14 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -import { Chip, Tab, Tabs, styled, Box } from '@mui/material'; +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/util/TabPanel'; +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'; @@ -27,33 +27,8 @@ import { useGetVisibleLibraryMangas } from '@/components/library/useGetVisibleLi import { SelectionFAB } from '@/components/collection/SelectionFAB.tsx'; import { PARTIAL_MANGA_FIELDS } from '@/lib/graphql/Fragments.ts'; import { MangaActionMenuItems } from '@/components/manga/MangaActionMenuItems.tsx'; - -const StyledGridWrapper = styled(Box)(({ theme }) => ({ - // TabsMenu height + TabsMenu bottom padding - grid item top padding - marginTop: `calc(48px + 13px - 8px)`, - // header height - TabsMenu height - TabsMenu bottom padding + grid item top padding - minHeight: 'calc(100vh - 64px - 48px - 13px + 8px)', - position: 'relative', - [theme.breakpoints.down('sm')]: { - // TabsMenu - 8px margin diff header height (56px) + TabsMenu bottom padding - grid item top padding - marginTop: `calc(48px - 8px + 13px - 8px)`, - // header height (+ 8px margin) - footer height - TabsMenu height - minHeight: 'calc(100vh - 64px - 64px - 48px)', - }, -})); - -const TabsMenu = styled(Tabs)(({ theme }) => ({ - display: 'flex', - position: 'fixed', - top: '64px', - width: 'calc(100% - 64px)', - zIndex: 1, - backgroundColor: theme.palette.background.default, - [theme.breakpoints.down('sm')]: { - top: '56px', // header height - width: '100%', - }, -})); +import { TabsMenu } from '@/components/tabs/TabsMenu.tsx'; +import { TabsWrapper } from '@/components/tabs/TabsWrapper.tsx'; const TitleWithSizeTag = styled('span')({ display: 'flex', @@ -209,22 +184,13 @@ export function Library() { ); } - // Visual Hack: 160px is min-width for viewport width of >600 - const scrollableTabs = window.innerWidth < tabs.length * 160; - return ( <> - + handleTabChange(newTab)} - indicatorColor="primary" - textColor="primary" - centered={!scrollableTabs} - variant={scrollableTabs ? 'scrollable' : 'fullWidth'} - scrollButtons - allowScrollButtonsMobile + tabsCount={tabs.length} > {tabs.map((tab) => ( ))} - + {isSelectModeActive && ( {(handleClose, setHideMenu) => ( diff --git a/src/screens/Sources.tsx b/src/screens/Sources.tsx index 04b92ec8..682dba9f 100644 --- a/src/screens/Sources.tsx +++ b/src/screens/Sources.tsx @@ -7,7 +7,7 @@ */ import { Fragment, useContext, useEffect } from 'react'; -import { IconButton, Tooltip } from '@mui/material'; +import { IconButton, Tooltip, Typography } from '@mui/material'; import TravelExploreIcon from '@mui/icons-material/TravelExplore'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; @@ -107,9 +107,18 @@ export function Sources() { ([lang, list]) => shownLangs.indexOf(lang) !== -1 && ( -

+ {translateExtensionLanguage(lang)} -

+
{(list as ISource[]) .filter((source) => showNsfw || !source.isNsfw) .map((source) => ( diff --git a/src/screens/Updates.tsx b/src/screens/Updates.tsx index 3fbe58aa..2840ea0f 100644 --- a/src/screens/Updates.tsx +++ b/src/screens/Updates.tsx @@ -7,7 +7,7 @@ */ import DownloadIcon from '@mui/icons-material/Download'; -import { Box, CardActionArea, styled, Tooltip } from '@mui/material'; +import { Box, CardActionArea, Tooltip } from '@mui/material'; import Avatar from '@mui/material/Avatar'; import Card from '@mui/material/Card'; import CardContent from '@mui/material/CardContent'; @@ -17,7 +17,6 @@ import React, { useCallback, useContext, useEffect, useLayoutEffect, useMemo, us import { Link, useLocation } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { t as translate } from 'i18next'; -import { GroupedVirtuoso } from 'react-virtuoso'; import { requestManager } from '@/lib/requests/RequestManager.ts'; import { LoadingPlaceholder } from '@/components/util/LoadingPlaceholder'; import { EmptyView } from '@/components/util/EmptyView'; @@ -26,40 +25,9 @@ import { DownloadType } from '@/lib/graphql/generated/graphql.ts'; import { TChapter } from '@/typings.ts'; import { NavBarContext } from '@/components/context/NavbarContext.tsx'; import { UpdateChecker } from '@/components/library/UpdateChecker.tsx'; - -const StyledGroupedVirtuoso = styled(GroupedVirtuoso, { shouldForwardProp: (prop) => prop !== 'heightToSubtract' })<{ - heightToSubtract: number; -}>(({ theme, heightToSubtract }) => ({ - // 64px header - height: `calc(100vh - 64px - ${heightToSubtract}px)`, - [theme.breakpoints.down('sm')]: { - // 64px header (margin); 64px menu (margin); - height: `calc(100vh - 64px - 64px - ${heightToSubtract}px)`, - }, -})); - -const StyledGroupHeader = styled(Typography, { shouldForwardProp: (prop) => prop !== 'isFirstItem' })<{ - isFirstItem: boolean; -}>(({ theme, isFirstItem }) => ({ - paddingLeft: '24px', - // 16px - 10px (bottom padding of the group items) - paddingTop: '6px', - paddingBottom: '16px', - fontWeight: 700, - textTransform: 'uppercase', - backgroundColor: theme.palette.background.default, - [theme.breakpoints.down('sm')]: { - // 16px - 8px (margin of header) - paddingTop: isFirstItem ? '8px' : '6px', - }, -})); - -const StyledGroupItemWrapper = styled(Box, { shouldForwardProp: (prop) => prop !== 'isLastItem' })<{ - isLastItem: boolean; -}>(({ isLastItem }) => ({ - padding: '0 10px', - paddingBottom: isLastItem ? '0' : '10px', -})); +import { StyledGroupedVirtuoso } from '@/components/virtuoso/StyledGroupedVirtuoso.tsx'; +import { StyledGroupHeader } from '@/components/virtuoso/StyledGroupHeader.tsx'; +import { StyledGroupItemWrapper } from '@/components/virtuoso/StyledGroupItemWrapper.tsx'; function epochToDate(epoch: number) { const date = new Date(0); // The 0 there is the key, which sets the date to the epoch