From 13cc34ffbf03e02aa54bcdea6745626b31edfa6a Mon Sep 17 00:00:00 2001 From: Maxime Bret Date: Wed, 31 Jul 2024 16:23:01 +0200 Subject: [PATCH] feat: migrated books to virtuoso --- packages/web/src/books/bookList/BookList.tsx | 236 ++++++++++-------- .../src/books/bookList/BookListGridItem.tsx | 16 +- .../src/collections/list/CollectionList.tsx | 18 +- .../src/common/lists/ListActionsToolbar.tsx | 2 +- .../web/src/common/lists/VirtuosoList.tsx | 136 ++++++---- .../web/src/home/ContinueReadingSection.tsx | 5 +- packages/web/src/home/HomeBookList.tsx | 3 +- .../web/src/home/RecentlyAddedSection.tsx | 5 +- packages/web/src/index.css | 12 +- .../src/library/books/LibraryBooksScreen.tsx | 44 ++-- .../shelves/LibraryCollectionScreen.tsx | 1 + .../src/library/tags/LibraryTagsScreen.tsx | 1 + packages/web/src/tags/tagList/TagList.tsx | 13 +- 13 files changed, 288 insertions(+), 204 deletions(-) diff --git a/packages/web/src/books/bookList/BookList.tsx b/packages/web/src/books/bookList/BookList.tsx index 5eab7e45..4d1e6a5f 100644 --- a/packages/web/src/books/bookList/BookList.tsx +++ b/packages/web/src/books/bookList/BookList.tsx @@ -1,143 +1,171 @@ -import React, { useCallback, FC, memo, ReactNode } from "react" -import { Box, useTheme } from "@mui/material" +import React, { useCallback, FC, memo, ReactNode, ComponentProps } from "react" +import { Box, BoxProps, Stack, useTheme } from "@mui/material" import { useWindowSize } from "react-use" import { BookListGridItem } from "./BookListGridItem" import { LibrarySorting } from "../../library/states" import { BookListListItem } from "./BookListListItem" -import { ReactWindowList } from "../../common/lists/ReactWindowList" import { ListActionViewMode } from "../../common/lists/ListActionsToolbar" import { BookListCompactItem } from "./BookListCompactItem" import { useListItemHeight } from "./useListItemHeight" +import { VirtuosoList } from "../../common/lists/VirtuosoList" const ItemListContainer = ({ children, isLast, - borders = false + borders = false, + ...rest }: { children: ReactNode isLast: boolean borders?: boolean -}) => ( +} & BoxProps) => ( {children} ) -export const BookList: FC<{ - viewMode?: ListActionViewMode - renderHeader?: () => React.ReactNode - headerHeight?: number - sorting?: LibrarySorting - isHorizontal?: boolean - style?: React.CSSProperties - itemWidth?: number - data: string[] - density?: "dense" | "large" - onItemClick?: (id: string) => void - withBookActions?: boolean - static?: boolean -}> = memo((props) => { - const { - viewMode = "grid", - renderHeader, - headerHeight, - density = "large", - isHorizontal = false, - style, - data, - itemWidth, - onItemClick, - withBookActions - } = props - const windowSize = useWindowSize() - const theme = useTheme() - const dynamicNumberOfItems = Math.round(windowSize.width / 200) - const itemsPerRow = - viewMode === "grid" && !isHorizontal - ? dynamicNumberOfItems > 0 - ? dynamicNumberOfItems - : dynamicNumberOfItems - : 1 - const adjustedRatioWhichConsiderBottom = theme.custom.coverAverageRatio - 0.1 - const { itemHeight, itemMargin } = useListItemHeight({ - density, - viewMode - }) +export const BookList = memo( + ( + props: { + viewMode?: ListActionViewMode + sorting?: LibrarySorting + isHorizontal?: boolean + itemWidth?: number + density?: "dense" | "large" + onItemClick?: (id: string) => void + withBookActions?: boolean + static?: boolean + } & ComponentProps + ) => { + const { + viewMode = "grid", + density = "large", + isHorizontal = false, + style, + data, + itemWidth, + onItemClick, + withBookActions, + static: isStatic, + ...rest + } = props + const windowSize = useWindowSize() + const theme = useTheme() + const dynamicNumberOfItems = Math.round(windowSize.width / 200) + const itemsPerRow = + viewMode === "grid" && !isHorizontal + ? dynamicNumberOfItems > 0 + ? dynamicNumberOfItems + : dynamicNumberOfItems + : 1 + const adjustedRatioWhichConsiderBottom = + theme.custom.coverAverageRatio - 0.1 + const { itemHeight, itemMargin } = useListItemHeight({ + density, + viewMode + }) + const computedItemWidth = itemWidth + ? itemWidth + : Math.floor(windowSize.width / itemsPerRow) + const computedItemHeight = + itemHeight || + Math.floor(computedItemWidth / adjustedRatioWhichConsiderBottom) - // const rowBorderColor = theme.palette.grey[100] + const rowRenderer = useCallback( + (index: number, item: string, { size }: { size: number }) => { + const isLast = index === size - 1 - const rowRenderer = useCallback( - (item: string, _: number, isLast: boolean) => { - return viewMode === "grid" ? ( - - ) : viewMode === "list" ? ( - - - - ) : ( - - - + ) : viewMode === "list" ? ( + + + + ) : ( + + + + ) + }, + [ + viewMode, + itemHeight, + itemMargin, + onItemClick, + withBookActions, + computedItemHeight + ] + ) + + if (isStatic) { + return ( + + {data?.map((item, index) => ( + + {rowRenderer(index, item, { size: data.length })} + + ))} + ) - }, - [viewMode, itemHeight, itemMargin, onItemClick, withBookActions] - ) + } - if (props.static) { return ( - - {data.map((item, index) => ( - - {rowRenderer(item, index, index === data.length - 1)} - - ))} - + + + ) - } - return ( - - - - ) -}) + // return ( + // + // + // + // ) + } +) diff --git a/packages/web/src/books/bookList/BookListGridItem.tsx b/packages/web/src/books/bookList/BookListGridItem.tsx index 0d1f2496..d8c10201 100644 --- a/packages/web/src/books/bookList/BookListGridItem.tsx +++ b/packages/web/src/books/bookList/BookListGridItem.tsx @@ -1,4 +1,4 @@ -import { FC, memo } from "react" +import { ComponentProps, FC, memo } from "react" import { Box, Typography, styled, useTheme } from "@mui/material" import { MoreVert } from "@mui/icons-material" import { bookActionDrawerSignal } from "../drawer/BookActionsDrawer" @@ -8,9 +8,8 @@ import { BookListCoverContainer } from "./BookListCoverContainer" import { useCSS } from "../../common/utils" import { getMetadataFromBook } from "../metadata" -const ContainerBox = styled("div")` +const ContainerBox = styled(Box)` cursor: pointer; - height: 100%; position: relative; display: flex; flex-direction: column; @@ -18,10 +17,12 @@ const ContainerBox = styled("div")` -webkit-tap-highlight-color: transparent; ` -export const BookListGridItem: FC<{ - bookId: string - onItemClick?: (id: string) => void -}> = memo(({ bookId, onItemClick }) => { +export const BookListGridItem: FC< + { + bookId: string + onItemClick?: (id: string) => void + } & ComponentProps +> = memo(({ bookId, onItemClick, ...rest }) => { const { data: item } = useBook({ id: bookId }) @@ -37,6 +38,7 @@ export const BookListGridItem: FC<{ if (onItemClick) return onItemClick(bookId) return onDefaultItemClick(bookId) }} + {...rest} > ["viewMode"] static?: boolean - } & Pick< - ComponentProps, - "onStateChange" | "restoreStateFrom" | "data" | "style" | "renderHeader" - > + } & ComponentProps > = memo(({ itemMode, ...props }) => { const { viewMode, data, onItemClick, static: isStatic, ...rest } = props const windowSize = useWindowSize() @@ -52,7 +49,7 @@ export const CollectionList: FC< if (isStatic) { return ( - {data.map((item, index) => ( + {data?.map((item, index) => ( {rowRenderer(index, item)} @@ -69,4 +66,13 @@ export const CollectionList: FC< {...rest} /> ) + + // return ( + // + // ) }) diff --git a/packages/web/src/common/lists/ListActionsToolbar.tsx b/packages/web/src/common/lists/ListActionsToolbar.tsx index ac97454a..de0a0e4f 100644 --- a/packages/web/src/common/lists/ListActionsToolbar.tsx +++ b/packages/web/src/common/lists/ListActionsToolbar.tsx @@ -10,7 +10,7 @@ import { import { SortByDialog } from "../../books/bookList/SortByDialog" export type ListActionSorting = ComponentProps["value"] -export type ListActionViewMode = "grid" | "list" | "compact" +export type ListActionViewMode = "grid" | "list" | "compact" | "horizontal" export const ViewModeIconButton = ({ viewMode, diff --git a/packages/web/src/common/lists/VirtuosoList.tsx b/packages/web/src/common/lists/VirtuosoList.tsx index 4ad39411..d30f0e82 100644 --- a/packages/web/src/common/lists/VirtuosoList.tsx +++ b/packages/web/src/common/lists/VirtuosoList.tsx @@ -1,7 +1,5 @@ import React, { - FC, memo, - ComponentProps, useRef, forwardRef, useMemo, @@ -11,6 +9,8 @@ import React, { } from "react" import { Box, Stack } from "@mui/material" import { + GridItemProps, + GridListProps, GridStateSnapshot, StateSnapshot, Virtuoso, @@ -18,42 +18,70 @@ import { VirtuosoGridHandle, VirtuosoHandle } from "react-virtuoso" +import { signal, useSignalValue } from "reactjrx" -export const VirtuosoList: FC<{ - renderHeader?: () => React.ReactNode - style?: React.CSSProperties - data: string[] - itemsPerRow: number - rowRenderer: (rowIndex: number, item: string) => React.ReactNode - onStateChange?: ( - state: - | { type: "list"; state: StateSnapshot } - | { type: "grid"; state: GridStateSnapshot } - ) => void - restoreStateFrom?: +type Context = { size: number } + +const restoreScrollSignal = signal< + Record< + string, | { type: "list"; state: StateSnapshot } | { type: "grid"; state: GridStateSnapshot } -}> = memo( + > +>({ + key: "restoreScrollSignal", + default: {} +}) + +export const VirtuosoList = memo( ({ renderHeader, - itemsPerRow, + itemsPerRow = 1, style, - data, + data = [], rowRenderer, onStateChange, restoreStateFrom, + restoreScrollId, + horizontalDirection, ...rest + }: { + renderHeader?: () => React.ReactNode | JSX.Element + style?: React.CSSProperties + data?: string[] + itemsPerRow?: number + restoreScrollId?: string + rowRenderer?: ( + rowIndex: number, + item: string, + context: Context + ) => React.ReactNode + horizontalDirection?: boolean + onStateChange?: ( + state: + | { type: "list"; state: StateSnapshot } + | { type: "grid"; state: GridStateSnapshot } + ) => void + restoreStateFrom?: + | { type: "list"; state: StateSnapshot } + | { type: "grid"; state: GridStateSnapshot } }) => { const virtuosoRef = useRef(null) const virtuosoGridRef = useRef(null) const [isReadyToBeShown, setIsReadyToBeShown] = useState(false) - const restoreStateFromFirstValue = useRef(restoreStateFrom) + const restoreScrollState = useSignalValue(restoreScrollSignal, (state) => + restoreScrollId ? state[restoreScrollId] : undefined + ) + const restoreStateFromFirstValue = useRef(restoreScrollState) - const GridListComponent: NonNullable< - ComponentProps["components"] - >["List"] = useMemo( + const GridListComponent = useMemo( () => - forwardRef(({ children, ...props }, ref) => ( + forwardRef< + any, + GridListProps & { + context?: Context + } + >(({ children, ...props }, ref) => ( {children} @@ -61,11 +89,15 @@ export const VirtuosoList: FC<{ [] ) - const GridItem: NonNullable< - ComponentProps["components"] - >["Item"] = useMemo( + const GridItem = useMemo( () => - ({ children, ref, ...props }) => ( + ({ + children, + ref, + ...props + }: GridItemProps & { + context?: Context + }) => ( ({ size }), [size]) + return ( <> {itemsPerRow > 1 ? ( { - onStateChange?.({ - state: { - gap: { column: 0, row: 0 }, - item: { height: 0, width: 0 }, - viewport: { height: 0, width: 0 }, - scrollTop: (event.target as HTMLDivElement).scrollTop - }, - type: "grid" - }) + if (restoreScrollId !== undefined) { + restoreScrollSignal.setValue((state) => ({ + ...state, + [restoreScrollId]: { + state: { + gap: { column: 0, row: 0 }, + item: { height: 0, width: 0 }, + viewport: { height: 0, width: 0 }, + scrollTop: (event.target as HTMLDivElement).scrollTop + }, + type: "grid" + } + })) + } }} // stateChanged={(state) => { // onStateChange?.({ @@ -144,7 +187,9 @@ export const VirtuosoList: FC<{ components={{ Header: renderHeader }} + context={context} data={data} + horizontalDirection={horizontalDirection} itemContent={rowRenderer} // {...(restoreStateFromFirstValue.current?.type === "list" && { // restoreStateFrom: restoreStateFromFirstValue.current.state @@ -155,13 +200,18 @@ export const VirtuosoList: FC<{ })} onScroll={(event) => { virtuosoRef.current?.getState((state) => { - onStateChange?.({ - state: { - ranges: [], - scrollTop: (event.target as HTMLDivElement).scrollTop - }, - type: "list" - }) + if (restoreScrollId !== undefined) { + restoreScrollSignal.setValue((state) => ({ + ...state, + [restoreScrollId]: { + state: { + ranges: [], + scrollTop: (event.target as HTMLDivElement).scrollTop + }, + type: "list" + } + })) + } }) }} {...rest} diff --git a/packages/web/src/home/ContinueReadingSection.tsx b/packages/web/src/home/ContinueReadingSection.tsx index a7874d45..b0daa0b6 100644 --- a/packages/web/src/home/ContinueReadingSection.tsx +++ b/packages/web/src/home/ContinueReadingSection.tsx @@ -13,7 +13,10 @@ export const ContinueReadingSection = memo(() => { Continue reading - + )} diff --git a/packages/web/src/home/HomeBookList.tsx b/packages/web/src/home/HomeBookList.tsx index 513408eb..09a45347 100644 --- a/packages/web/src/home/HomeBookList.tsx +++ b/packages/web/src/home/HomeBookList.tsx @@ -16,10 +16,9 @@ export const HomeBookList = memo((props: ComponentProps) => { return ( ) diff --git a/packages/web/src/home/RecentlyAddedSection.tsx b/packages/web/src/home/RecentlyAddedSection.tsx index 0b30e1b4..6c0afe96 100644 --- a/packages/web/src/home/RecentlyAddedSection.tsx +++ b/packages/web/src/home/RecentlyAddedSection.tsx @@ -13,7 +13,10 @@ export const RecentlyAddedSection = memo(() => { Recently added - + )} diff --git a/packages/web/src/index.css b/packages/web/src/index.css index b9e6b43e..d6fd57cd 100644 --- a/packages/web/src/index.css +++ b/packages/web/src/index.css @@ -10,7 +10,6 @@ body { sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - scrollbar-width: none; } html, @@ -24,6 +23,9 @@ body { *::before, *::after { box-sizing: border-box; +} + +*:not(.withScrollBar) { scrollbar-width: none; } @@ -37,10 +39,12 @@ code { monospace; } -::-webkit-scrollbar { - width: 0px; /* Remove scrollbar space */ +::-webkit-scrollbar:not(.withScrollBar) { + /* Remove scrollbar space */ + width: 0px; height: 0; - background: transparent; /* Optional: just make scrollbar invisible */ + /* optional: just make scrollbar invisible; */ + background: transparent; } .sentry-error-embed-wrapper { diff --git a/packages/web/src/library/books/LibraryBooksScreen.tsx b/packages/web/src/library/books/LibraryBooksScreen.tsx index 6c7c60e2..625bec2e 100644 --- a/packages/web/src/library/books/LibraryBooksScreen.tsx +++ b/packages/web/src/library/books/LibraryBooksScreen.tsx @@ -7,7 +7,8 @@ import { Badge, Typography, useTheme, - Box + Box, + Stack } from "@mui/material" import { TuneRounded, SortRounded } from "@mui/icons-material" import { LibraryFiltersDrawer } from "../LibraryFiltersDrawer" @@ -61,30 +62,16 @@ export const LibraryBooksScreen = () => { ) const listHeader = useMemo( - () => ( - - {addBookButton} - - ), - [theme, addBookButton] + () => {addBookButton}, + [addBookButton] ) const bookListRenderHeader = useCallback(() => listHeader, [listHeader]) - const [listHeaderDimTracker, { height: listHeaderHeight }] = - useMeasureElement(listHeader) - useEffect(() => () => isUploadBookDrawerOpenedStateSignal.setValue(false), []) return (
- {listHeaderDimTracker} { }} /> - {books.length === 0 && (
{ )} { isUploadBookFromDataSourceDialogOpenedSignal.setValue(type) }} /> - +
) } diff --git a/packages/web/src/library/shelves/LibraryCollectionScreen.tsx b/packages/web/src/library/shelves/LibraryCollectionScreen.tsx index d4abd725..df979ea3 100644 --- a/packages/web/src/library/shelves/LibraryCollectionScreen.tsx +++ b/packages/web/src/library/shelves/LibraryCollectionScreen.tsx @@ -75,6 +75,7 @@ export const LibraryCollectionScreen = () => { viewMode={viewMode} onStateChange={libraryCollectionScreenPreviousScrollState.setValue} restoreStateFrom={libraryCollectionScreenPreviousScroll} + restoreScrollId="libraryShelves" /> )} { height: "100%" }} data={tags} + restoreScrollId="libraryTagList" renderHeader={listRenderHeader} onItemClick={(tag) => { const action = () => setIsTagActionsDrawerOpenedWith(tag?._id) diff --git a/packages/web/src/tags/tagList/TagList.tsx b/packages/web/src/tags/tagList/TagList.tsx index 9138f2d8..519ee948 100644 --- a/packages/web/src/tags/tagList/TagList.tsx +++ b/packages/web/src/tags/tagList/TagList.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, memo, ComponentProps } from "react" +import { useCallback, memo, ComponentProps } from "react" import { TagListItemList } from "./TagListItemList" import { VirtuosoList } from "../../common/lists/VirtuosoList" @@ -7,14 +7,8 @@ const itemStyle = { height: 60 } export const TagList = memo( ( props: { - renderHeader?: () => React.ReactNode - style?: React.CSSProperties - data: string[] onItemClick?: (tag: { _id: string; isProtected: boolean }) => void - } & Pick< - ComponentProps, - "onStateChange" | "restoreStateFrom" | "data" | "style" | "renderHeader" - > + } & ComponentProps ) => { const { onItemClick, ...rest } = props @@ -30,5 +24,8 @@ export const TagList = memo( ) return + // return ( + // + // ) } )